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

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

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

d会在 code that takes a while部分占用内存吗?

11125 次浏览

不,大括号不作为堆栈框架。在 C 语言中,大括号只表示一个命名范围,但是没有任何东西被销毁,当控制从栈中传出时也没有任何东西从栈中弹出。

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

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

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

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

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

不,对于例程的其余部分,d []将位于堆栈上。但是 alloca ()是不同的。

编辑: 克里斯托弗 · 约翰逊(以及西蒙和丹尼尔)是 ,我最初的反应是 错误。与 gcc 4.3.4。在 CYGWIN 上,密码是:

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 关于多次分配也是正确的。

后来又添加了 : 上面的测试显示 海湾合作委员会文件不完全正确。多年来,它一直在强调:

“一旦数组名称为 范围 结束,可变长数组的空间就是 已释放。”

你的问题不够清楚,不能明确地回答。

一方面,编译器通常不会为嵌套块作用域执行任何本地内存分配-释放。本地内存通常只在函数进入时分配一次,在函数退出时释放。

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

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

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

后者是否有资格作为 d继续占用记忆,直到功能结束在您的问题的上下文中是由您来决定。

变量 d通常不会从堆栈中弹出。大括号不表示堆栈帧。否则,你就不能做这样的事情:

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

如果大括号导致了真正的堆栈推送/弹出(就像函数调用一样) ,那么上面的代码将不能编译,因为大括号内的代码将不能访问大括号外的变量 var(就像子函数不能直接访问调用函数中的变量一样)。我们知道事实并非如此。

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

更新: 以下是 C 规格的说明: 关于自动存储持续时间的对象(6.4.2节) :

对于不具有可变长度数组类型的对象,其 生命周期从入口延伸到与其关联的块 直到该块的执行以任何方式结束。

同一部分将术语“生命周期”定义为(重点是我的) :

对象的 一辈子是程序执行期间的部分 保留哪个存储器是 保证, 具有常量地址,并在整个过程中始终保留其最后存储的值 如果一个对象在其生存期之外被引用,则 行为是未定义的。

当然,这里的关键词是“保证”。一旦离开内部大括号集的作用域,数组的生命周期就结束了。存储空间可能仍然会被分配给它,也可能不会(编译器可能会为其他事情重用这个空间) ,但是任何访问数组的尝试都会调用未定义行为,并导致不可预测的结果。

C 规范没有堆栈帧的概念。它只说明结果程序将如何运行,并将实现细节留给编译器(毕竟,在无栈 CPU 上的实现与在有硬件栈的 CPU 上的实现看起来完全不同)。在 C 规范中,没有任何东西强制要求堆栈框架在哪里结束或不结束。了解 真的的唯一方法是在特定的编译器/平台上编译代码并检查生成的程序集。编译器当前的一组优化选项可能也会在其中发挥作用。

如果您想确保数组 d在代码运行时不再占用内存,您可以将大括号中的代码转换为单独的函数,或者显式地将内存 mallocfree转换为自动存储。

有可能。他们可能不会。我认为您真正需要的答案是: 永远不要假设任何事情。现代编译器具有各种架构和特定于实现的魔力。为人类编写简单易懂的代码,让编译器完成好的工作。如果你试图围绕编译器编写代码,你就是在自找麻烦——在这种情况下,你通常会遇到的麻烦通常是非常微妙和难以诊断的。

变量占用内存的时间显然与编译器有关(当函数中的内部块进入和退出时,许多编译器不调整堆栈指针)。

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

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


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


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


return;
}

(换句话说: 是编译器 允许释放 d,即使在实践中大多数不?)。

答案是,编译器 允许释放 d,并且在注释指示的地方访问 p[0]是未定义的行为(程序 没有允许访问内部作用域之外的内部对象)。C 标准的相关部分是6.2.4 p5:

对于这样一个物体[有 自动存储时间] 没有可变长度的数组类型, 它的生命周期从入口延伸到与它相关联的块 直到该块的执行结束 任何方式 或者调用函数挂起,但是 不结束,则执行当前 )如果输入块 类的新实例 每次都会创建一个 对象的初始值为 不确定。如果初始化是 为该对象指定的,则为 每次声明 在执行该块时达到的; 否则,值将变为 不确定 达成声明。

已经有很多关于该标准的信息表明它确实是 具体措施。

因此,一个实验可能会引起我们的兴趣,如果我们尝试以下代码:

#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 我们可以得到两个相同的地址: 科利罗

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

#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 我们在这里获得两个不同的地址: 科利罗

所以你不能确定到底发生了什么。