何时完全堆叠

时间:2016-12-05 12:13:57

标签: c++ c stack

即使在C(不仅仅是C ++)中,您也可以在代码块的开头声明变量,该代码块用大括号括起来。

示例:

#include <stdio.h>
void use_stack(int cnt)
{
    if (cnt<=16) {
        int a[16];
        int i;
        a[0]=3;
        a[1]=5;
        for (i=2;i<cnt;i++) {
            a[i]=a[i-1]+a[i-2];
        }
        printf("a[%d] == %d\n",cnt-1,a[cnt-1]);
    }
    else {
        printf("cnt is too big\n");
    }
}

现在我知道像数组a[16]这样的变量在这种情况下会在堆栈上分配。

我想知道这个数组的空间是在函数的开头(第一个打开大括号)还是在声明它的块的开头分配(在if之后打开大括号)。< / p>

从检查汇编代码开始,编译器似乎直接在函数的入口处为a[16]分配了空间。

我实际上期望在a[16]的声明处分配堆栈(堆栈指针减少)并且堆栈将在相应的{{1}的末尾被解除分配(堆栈指针增加)代码块。

但是这似乎没有发生(if的堆栈直接在函数入口处分配,即使a[16]分支中没有使用a[16]。)

有没有人解释为什么会这样?

C语言标准的任何部分都可以解释这种行为,或者它与#34; longjmp&#34;等等有关。或信号处理,这可能要求堆栈指针是“常数”#34;在函数内部?

注意:我假设堆栈的原因是在代码块的开头/结尾分配/解除分配,因为在C ++中,在堆栈上分配的对象的构造函数/析构函数将在开始/结束时调用代码块。因此,如果您检查C ++程序的汇编代码,您会注意到堆栈仍然在函数入口处分配;只需构造函数/析构函数调用将在代码块的开头/结尾完成。

我明确感兴趣的是为什么堆栈没有使用花括号在代码块的开头/结尾分配/解除分配。

问题:At what exact moment is a local variable allocated storage?仅涉及在函数开头分配的局部变量。我很惊讶稍后在代码块中分配的变量的堆栈分配也在函数入口处完成。

到目前为止,答案是:

  • 与优化有关的事情
  • 可能与C,C ++不同
  • C语言规范中甚至没有提到Stack

所以:我对C的答案感兴趣...(我坚信答案也适用于C ++,但我不是在问C ++: - ))。

优化:这是一个例子,它将直接说明为什么我如此惊讶,以及为什么我非常确定这是优化:

else

我知道这是一个丑陋的程序: - )。

函数#include <stdio.h> static char *stackA; static char *stackB; static void calc(int c,int *array) { int result; if (c<=0) { // base case c<=0: stackB=(char *)(&result); printf("stack ptr calc() = %p\n",stackB); if (array==NULL) { printf("a[0] == 1\n"); } else { array[0]=1; } return; } // here: c>0 if (array==NULL) { // no array allocated, so allocate it now int i; int a[2500]; // calculate array entries recursively calc(c-1,a); // calculate current array entry a[c] a[c]=a[c-1]+3; // print full array for(i=0;i<=c;i++) { printf("a[%d] = %d\n",i,a[i]); } } else { // array already allocated calc(c-1,array); // calculate current array entry a[c] array[c]=array[c-1]+3; } } int main() { int a; stackA=(char *)(&a); printf("stack ptr main() = %p\n",stackA); calc(9,NULL); printf("used stack = %d\n",(int)(stackA-stackB)); } 以递归方式计算所有calc的{​​{1}}。

如果查看n*3 + 1的代码,您会注意到只有当函数的输入参数0<=n<=ccalc时才会声明数组a[2500]

现在这只发生在array的{​​{1}}调用中。

NULLcalc指针用于计算粗略估计此程序使用多少堆栈。

现在:main应该消耗大约10000个字节(每个整数4个字节,2500个条目)。所以你可以期望整个程序消耗大约10000字节的堆栈+额外的东西(对于递归调用stackA时的开销)。

但是:事实证明这个程序消耗了大约100000字节的堆栈(是预期的10倍)。原因是,对于stackB int a[2500]调用calc,即使它仅用于第一次调用。有10次调用calca[2500]),因此您消耗了100000字节的堆栈。

  • 如果您使用或不使用优化来编译程序,就很重要
  • GCC-4.8.4和clang for x64,MS Visual C ++ 2010,Windriver for DIAB(for PowerPC) all 表现出这种行为

甚至更奇怪:C99引入了可变长度数组。如果我将上述代码中的calc替换为0<=c<=9,则程序将使用较少堆栈空间(减少约90000字节)。

注意:如果我int a[2500];int a[2500+c];的调用更改为calc,程序将崩溃(堆栈溢出==分段错误)。如果我另外更改为main,则程序可以正常工作并使用少于100KB的堆栈。我仍然希望看到一个答案,这解释了为什么可变长度数组导致堆栈溢出,而固定长度数组确实导致堆栈溢出,特别是因为这个固定长度数组是超出范围(第一次调用calc(1000,NULL);除外)。

那么在C中出现这种行为的原因是什么?

我不相信GCC / clang都不能做得更好;我坚信必须有技术上的理由。有什么想法吗?

Google的回答

经过更多的谷歌搜索:我坚信这与&#34; setjmp / longjmp&#34;行为。 Google for&#34; Variable Length Array longjmp&#34;并亲眼看看。如果你在函数入口处分配所有数组,似乎很难实现longjmp。

2 个答案:

答案 0 :(得分:0)

Alf已经解释了标准规定的限制和自由(或者更确切地说,它没有指定的内容)。

我建议回答问题

  

为什么堆栈没有使用花括号在代码块的开头/结尾分配/解除分配。

您测试的编译器选择这样做(我实际上只是猜测,我没有写任何一个),因为更好的运行时性能和简单性(当然,因为它是允许的。)

分配96个字节(任意示例)一次所需的时间大约是分配48个字节两次的一半。第三次分配32个字节三次。

将循环视为极端情况:

for(int i = 0; i < 1000000; i++) {
    int j;
}

如果在函数开头分配j,则有一个分配。如果在循环体内分配j,那么将有一百万个分配。分配越少越好(更快)。

  

注意:我假设堆栈的原因是在代码块的开头/结尾分配/解除分配,因为在C ++中,在堆栈上分配的对象的构造函数/析构函数将在开始/结束时调用代码块。

现在你知道假装你错了。如链接问题中的精细答案所述,分配/处理不一定与构造/破坏一致。

  

想象一下,我会使用a[100000]

即接近总堆栈空间的很大一部分。你应该动态分配大块内存。

答案 1 :(得分:0)

如何做到这一点不受任何标准的约束。 C和C ++标准根本没有提到堆栈,理论上这些语言甚至可以在没有堆栈的计算机上使用。

在具有堆栈的计算机上,如何完成此操作由给定系统的ABI指定。通常,在程序进入函数时保留堆栈空间。但是编译器可以自由地优化代码,这样只有在使用某个变量时才会保留堆栈空间。

无论如何,声明变量的点与分配时间无关。在您的示例中,int a[16]在输入函数时分配,或者在使用它的第一个位置之前分配。如果在if语句中或函数顶部声明a,则无关紧要。

但是,在C ++中,存在构造函数的概念。如果您的变量是带有构造函数的对象,那么该构造函数将在声明变量的位置执行。这意味着必须在此之前分配变量。