自动/静态内存分配

4
也许这是一个幼稚的问题,但...
确认或否认:自动和静态存储期对象/变量的内存存在是在编译时确定的,程序在运行时因为自动对象没有足够的内存而失败的可能性绝对为零。
当自动对象的构造函数执行动态分配并且此分配失败时,我们自然认为这是动态分配的失败,而不是自动分配的失败。

虽然静态对象可以在编译时进行确认,但对于自动对象来说则不行,因为它取决于程序逻辑。 - ruslik
这个问题的措辞方式确实听起来像是一道作业/考试题,但我不敢相信一个拥有8k声望值的人会向SO寻求帮助完成他的作业。 :) 不过我很好奇它来自哪里。 - R.. GitHub STOP HELPING ICE
@R.. 它是这样的:当您尝试分配一个太大的自动数组时,您会收到编译错误,但是当您尝试分配一个大的动态数组时,您会收到运行时错误。因此,我想知道,即使编译成功,是否可能在前一种情况下也会出现运行时错误 :) - Armen Tsirunyan
@Armen:你认为这个代码 int f(){int t=f();}; 会出现编译错误吗? - ruslik
@ruslik:不,我绝对没有。但我很可能会收到警告。 - Armen Tsirunyan
@Armen:在简单的情况下,编译器能够避免递归调用。但是当递归深度在编译时无法确定时,它将无法警告您。此外,在许多情况下,编译器甚至不知道堆栈大小。 - ruslik
6个回答

13

自动分配内存可能会失败 - 这通常被称为堆栈溢出。当某人试图将一个非常大的数组作为本地变量时,你经常会看到这种情况。不受限制(或者不足够受限)的递归也会导致此问题。

在平台无关的方式中,您实际上不能检测到自动分配失败并处理它。


检测自动分配失败并处理它,这包括您无法可移植地预测它。程序 int main() { int a = 1; int b = 2; return a + b;} 不是严格符合规范的,因为它依赖于在实现之间可能会有所不同的行为(具体来说,是否有足够的堆栈可用于两个整数),但正式上它并没有变化的行为,因为标准根本没有提到这个问题。相反,任何程序是否工作、在运行时以某种方式中止或破坏堆都超出了标准的范围。 - Steve Jessop

13

1
我必须说,我无法相信这个答案有两个赞。然后我让它变成了三个。 - Billy ONeal
@Prasoon:我怎么可能忘记那件事呢?! :) - Armen Tsirunyan
@Prasoon:抱歉拼错了。我会安排预约下周把脚从嘴里拿出来 :) - Billy ONeal

4
在具有过度提交功能(例如默认配置下的Linux)的系统中,即使是静态存储期对象也可能在运行时导致失败。在程序启动时,这些对象将存在于未初始化的写时复制零页或磁盘上可执行文件的写时复制映射中。在第一次尝试写入它们时,会发生页面错误,内核将为您的进程创建一个本地可修改的副本。如果内核粗心大意并没有保留与提交给进程的内存一样多的内存,则可能会失败,结果将是令人恐惧的OOM-killer。
没有强大的系统会出现这个问题,Linux的行为可以通过以下方式修复:
echo "2" > /proc/sys/vm/overcommit_memory

1
+1:非标准行为,由Linux带给您!感谢您使得使用默认设置创建符合规范的C实现变得不可能。*比尔安装BSD。 - Billy ONeal
我相信 overcommit 也是传统的 BSD 行为,但我可能错了。我不太了解不同的 BSD 版本,所以没有提到它们。 - R.. GitHub STOP HELPING ICE

2

不是这样的。自动分配可能会导致栈溢出,在我所知道的大多数体系结构/平台上都会导致进程立即终止。

此外,程序可能无法从底层平台分配足够的空间给静态变量,此时程序仍将失败,但它将在调用main之前失败。


@Billy:如果每个调用只增加几KB的堆栈使用量,你将会触发一个保护页并且进程将被终止。但是如果单个调用将堆栈增长2GB呢?除非编译器生成特殊代码来检查这一点(并减慢所有不需要它的理智函数),否则堆栈指针将最终停留在其他某些内存的中间,并且您的函数将愉快地破坏那些内存。 - R.. GitHub STOP HELPING ICE
@R..:我认为从技术上讲,只有那些总共移动堆栈指针超过警戒区域大小的函数会被减慢(假设编译器知道这一点,或者至少知道页面大小),或者使用 VLAs、alloca 或类似的东西。有人可能会认为,这样的函数本来就不太理智,或者无论如何,在这些东西上造成性能损失(默认情况下,可以通过编译器选项禁用)是值得的,考虑到它们在安全使用方面有多么棘手。 - Steve Jessop
只要函数调用将返回地址推入堆栈,您所担心的“小”数量累加的问题就不是问题了。我不认为任何实现可以处理超过一个小有限(即1步)级别的嵌套函数调用而不触及堆栈。至于编译器生成代码以检查堆栈状态,我真的不喜欢编译器/链接器/crt/libc/kernel等之间不必要的交叉依赖,特别是当所谓的好处微不足道且仅有助于可疑质量的代码时。 - R.. GitHub STOP HELPING ICE
@R. 无论内存如何被访问,你都会触发守卫页。我不明白实际函数调用与此有何关系。 - Billy ONeal
假设所有函数将它们的返回地址存储在相对于本地变量的相同位置。如果一个函数将其链接寄存器“放置在”本地数组上方,然后调用一个将本地数组“放置在”其实际涉及范围(包括其自己的链接寄存器,如果它进行进一步的调用)之上的函数,则每个函数可能只能将堆栈移动半个守护区域的大小(+ delta),但守护区域中的任何内容都不会被触及,因为守护区域仅包含这两个数组。所以我的“系列”调用是2。 - Steve Jessop
显示剩余6条评论

0

简单的反例:

#include <string.h>

int main()
{
    int huge[0x1FFFFFFF]; // Specific size doesn't matter;
                          // it just has to be bigger than the stack.

    memset(huge, 0, sizeof(huge) / sizeof(int));

    return 0;
}

栈帧大小通常是无限的。但是,栈本身的总大小通常是限制因素。 - Billy ONeal
@Armen:它应该能够被编译,但在运行时将会失败。尽管编译器可能看着代码想说:“你确定这是对的吗?”如果你试图在 32 位机器上编译它,它将会失败,因为 20 亿个整数比机器的整个内存空间还要大(大约多出两倍)。 - Billy ONeal
@Armen:现在它将自行编译。 - nmichaels
@Billy:嘿,你说得对。我忘记了我在使用64位机器。 - nmichaels
@R: 哎呀,你是对的。结果当然没有改变。这就是编写错误程序的麻烦所在。 - nmichaels

-1

例子:

#include <iostream>

using namespace std;

class A
{
public:
    A() { p = new int[0xFFFFFFFF]; }

private:
    int* p;
};

static A g_a;

int main()
{
    cout << "Why do I never get called?" << endl;
}

1
你错过了OP问题的最后一句话。 “当自动对象构造函数执行动态分配并且此类分配失败时,我们认为这是动态分配失败而不是自动分配。”编辑:using namespace std;需要消失! :P - Billy ONeal
在简单的示例中使用using namespace std;并不是一个问题。这使得示例比到处都有std::更易读。当然,在这个例子中,这并不重要,因为我只使用了1个cout语句(而且在它之前就发生了失败)。此外,他的问题涉及静态变量的创建。我使用了new,但你也可以像这样轻松地创建成员变量int p[0x7FFFFFFF]以获得相同的效果。无论哪种情况,都会导致堆栈溢出。 - Zac Howland
@Zac:1. 我认为这会使代码不太容易阅读。当你调用标准库时,应该在调用站点明显可见。2. 不,成员变量和对new的调用都不会导致堆栈溢出。第一种情况可能会失败,因为静态存储空间将被耗尽。对new的调用失败是因为堆已经耗尽。两种分配都没有接触到堆栈。 - Billy ONeal
@Billy 1. 那是一个宗教性的讨论,可以一直持续到审判日,所以简单地说,各有各的观点。2. 你忘记了堆栈溢出错误的完整文本:堆栈溢出。也就是说,如果你分配了太多的堆空间,它将与你的堆栈空间(静态存储空间同理)相交,因此,在任何情况下,都会导致同样的问题:你在任何给定的内存池中分配了太多的内存并破坏了其他内存池。 - Zac Howland
@Zac:那不是真的。简化的“栈向一侧增长,堆向另一侧增长”的说法在大多数实际机器上都不成立,因为大多数实际机器都有虚拟内存。是的,你可能会遇到内存越界的问题,但这并不意味着发生了栈溢出。只有自动变量才会存储在栈上,而你在那里没有自动变量。 - Billy ONeal
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接