在C语言中,花括号是否充当堆栈框架?


153

如果我在一组新的花括号中创建一个变量,那么该变量是从右括号中弹出的堆栈,还是一直挂到函数结尾?例如:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

d在本code that takes a while节中会占用内存吗?


8
您是指(1)根据标准,(2)在实施中普遍采用,还是(3)在实施中普遍采用?
David Thornley'5

Answers:


83

不,大括号不能用作堆栈框架。在C语言中,花括号仅表示命名范围,但是当控制权移出时,不会破坏任何内容,也不会弹出任何内容。

作为程序员编写代码,您通常可以将其视为堆栈框架。大括号内声明的标识符只能在大括号内访问,因此从程序员的角度来看,就像将标识符声明时将其压入堆栈,然后在退出作用域时将其弹出一样。但是,编译器不必生成在进入/退出时推送/弹出任何内容的代码(通常,它们不需要这样做)。

还要注意,局部变量可能根本不使用任何堆栈空间:它们可以保存在CPU寄存器中或其他辅助存储位置中,或者可以完全优化掉。

因此,从d理论上讲,数组可能会消耗整个函数的内存。但是,编译器可能会对其进行优化,或者与使用期限不重叠的其他局部变量共享其内存。


9
那不是特定于实现的吗?
2010年

54
在C ++中,对象的析构函数在其作用域的结尾被调用。内存是否被回收是特定于实现的问题。
克里斯托弗·约翰逊

8
@ pm100:将调用析构函数。那没什么关于那些对象占用的内存的。
多纳研究员,2010年

9
C标准规定,在块中声明的自动变量的生存期仅延长到块执行结束为​​止。因此,基本上,这些自动变量确实会在代码块的末尾被“破坏”。
caf 2010年

3
@KristopherJohnson:如果一个方法有两个单独的块,每个块声明一个1Kbyte的数组,而第三个块称为嵌套方法,则编译器可以自由地对两个数组使用相同的内存,和/或放置该数组在堆栈最浅的部分,将堆栈指针移到其上方,以调用嵌套方法。这种行为可能会使函数调用所需的堆栈深度减少2K。
supercat 2014年

39

变量实际占用内存的时间显然取决于编译器(并且在函数中进入和退出内部块时,许多编译器不会调整堆栈指针)。

但是,一个密切相关但可能更有趣的问题是,是否允许程序访问内部作用域之外(但在包含函数之内)的内部对象,即:

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(换句话说:即使实际上大多数情况下都不允许编译器进行dealloc d)?

答案是编译器允许解除分配d,并访问p[0]其中的注释表示是未定义的行为(程序没有允许访问的范围内的内对象外)。C标准的相关部分是6.2.4p5:

对于这种不具有可变长度数组类型的对象[具有自动存储持续时间的对象], 其生存期从进入与之关联的块的时间开始,直到该块的执行以任何方式结束。(进入一个封闭的块或调用一个函数会暂停,但不会结束当前块的执行。)如果递归地输入该块,则每次都会创建该对象的新实例。对象的初始值不确定。如果为对象指定了初始化,则每次在执行块时到达声明时都将执行;否则,每次到达声明时,该值将变得不确定。


经过多年使用高级语言的学习,人们逐渐了解了范围和内存在C和C ++中的工作方式,我发现这个答案比公认的答案更为精确和有用。
克里斯(Chris)

20

您的问题尚不清楚,无法明确回答。

一方面,编译器通常不会对嵌套块作用域进行任何本地内存分配-释放。通常,本地存储器仅在功能入口分配一次,并在功能出口释放。

另一方面,当本地对象的生存期结束时,该对象占用的内存可在以后重新用于另一个本地对象。例如,在此代码中

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

这两个阵列通常会占用相同的内存区域,这意味着函数所需的本地存储总量foo是两个阵列中最大的阵列所需的,而不是同时用于两个阵列。

d在您所考虑的问题中,后者是否符合继续占用内存直到功能结束的资格,由您决定。


6

它取决于实现。我编写了一个简短的程序来测试gcc 4.3.4的功能,它在函数开始时立即分配所有堆栈空间。您可以使用-S标志检查gcc生成的程序集。


3

不,d [] 在例程的其余部分中不会处于堆栈中。但是alloca()是不同的。

编辑:克里斯托弗·约翰逊(和克里斯汀·丹尼尔)是对的,而我最初的回答是错误的。在CYGWIN上使用gcc 4.3.4。时,代码:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

给出:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

活到老,学到老!快速测试似乎表明,AndreyT在多重分配方面也是正确的。

以后再添加:上述测试表明gcc文档不太正确。多年来,它一直在说(强调):

“ 一旦数组名称的作用域结束,就将释放可变长度数组的空间 。”


在禁用优化的情况下进行编译并不一定会向您显示优化后的代码。在这种情况下,行为是相同的(在函数开始时分配,并且仅在离开函数时释放): godbolt.org/g/M112AQ。但是非cygwin gcc不会调用alloca函数。cygwin gcc会做到这一点让我感到非常惊讶。它甚至不是可变长度的数组,所以IDK为什么要提起它。
彼得·科德斯

2

他们可能。他们可能不会。我认为您真正需要的答案是:永远不要承担任何责任。现代编译器执行各种体系结构和特定于实现的魔术。简单,易读地将代码编写给人类,让编译器完成这些工作。如果您尝试围绕编译器进行编码,那么您会遇到麻烦-而且在这种情况下通常遇到的麻烦通常非常微妙,难以诊断。


1

您的变量d通常不会从堆栈中弹出。花括号不表示堆栈框架。否则,您将无法执行以下操作:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

如果大括号引起了真正的堆栈推入/弹出操作(如函数调用一样),则上述代码将无法编译,因为大括号内的代码将无法访问var位于大括号外的变量(就像子函数一样)。函数不能直接访问调用函数中的变量)。我们知道事实并非如此。

花括号仅用于范围定义。编译器会将从括号括起来的外部对“内部”变量的任何访问都视为无效,并且它可能会将该内存重新用于其他用途(这取决于实现)。但是,直到封闭函数返回之前,它可能不会从堆栈中弹出。

更新: 这是C规范必须说的。关于具有自动存储期限的对象(第6.4.2节):

对于不具有可变长度数组类型的对象,其生存期从进入与之关联的块开始一直到该块的执行结束为​​止。

同一部分将“生存期”定义为(强调我的):

对象的生存期是程序执行的一部分,在此期间保证为其保留存储空间。一个对象存在,具有恒定的地址,并在其整个生命周期内保留其最后存储的值。如果在其生存期之外引用对象,则行为是不确定的。

当然,这里的关键词是“保证的”。离开内部括号的范围后,数组的生命周期就结束了。可能会或可能不会为它分配存储空间(您的编译器可能会重新使用该空间用于其他用途),但是任何访问数组的尝试都会导致未定义的行为并带来不可预测的结果。

C规范没有堆栈帧的概念。它仅说明结果程序的行为方式,并将实现细节留给编译器(毕竟,无堆栈CPU上的实现看起来与带硬件堆栈的CPU上的实现完全不同)。C规范中没有任何内容规定堆栈帧将在何处结束。唯一真正了解的方法是在特定的编译器/平台上编译代码并检查生成的程序集。您的编译器当前的优化选项集也可能在其中发挥作用。

如果你想确保阵列d不再吃了,而你的代码运行内存,可以在大括号中的代码或者转换成一个单独的函数或明确mallocfree内存,而不是使用自动存储。


1
“如果花括号导致堆栈推入/弹出,则上面的代码将无法编译,因为花括号内的代码将无法访问位于花括号外的变量var” –这是不正确的。编译器始终可以记住与堆栈/帧指针的距离,并使用它来引用外部变量。另外,请参阅约瑟夫的答案以获取确实引起堆栈推入/弹出的大括号示例。
乔治,2012年

@ george-您描述的行为以及约瑟夫的示例,取决于您使用的编译器和平台。例如,为MIPS目标编译相同的代码将产生完全不同的结果。我纯粹是从C规范的角度讲(因为OP没有指定编译器或目标)。我将编辑答案并添加更多细节。
bta 2012年

0

我相信它的确超出了范围,但是直到函数返回时才弹出堆栈。因此,在函数完成之前,它仍将占用堆栈上的内存,但在第一个闭合花括号的下游无法访问。


3
没有保证。一旦范围关闭,编译器就不再跟踪该内存了(或者至少不需要...),并且可以很好地重用它。这就是为什么触摸以前由超出范围的变量占用的内存是未定义行为。当心鼻恶魔和类似的警告。
dmckee ---前主持人小猫,2010年

0

已经有很多关于该标准的信息,表明它确实是特定于实现的。

因此,可能需要进行一项实验。如果我们尝试以下代码:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

使用gcc,我们在这里获得两次相同的地址:Coliro

但是,如果我们尝试以下代码:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

使用gcc我们在这里获得两个不同的地址:Coliro

因此,您无法确定到底发生了什么。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.