一个本地变量在什么时候分配存储空间?

9
假设我们有以下内容:
void print()
{
     int a;  // declaration
     a = 9;
     cout << a << endl;
}

int main ()
{
     print();
}

变量a的存储是在主函数调用print函数时分配的,还是在函数内部声明时执行到时分配的?

我猜你说的是堆栈上的存储,而不是寄存器,对吗? - Ponkadoodle
1
这个并没有定义。它完全取决于编译器。唯一定义的是对象的生命周期。它甚至可能根本没有分配任何内存。 - Martin York
5个回答

9

这在编译器内部非常依赖于编译器,但从逻辑上讲,变量声明时就已经分配了存储空间。

考虑下面这个简单的C++示例:

// junk.c++
int addtwo(int a)
{
    int x = 2;

    return a + x;
}

当GCC编译时,会生成以下代码(;我的注释):
.file   "junk.c++"
    .text
.globl _Z6addtwoi
    .type   _Z6addtwoi, @function
_Z6addtwoi:
.LFB2:
    pushl   %ebp           ;store the old stack frame (caller's parameters and locals)
.LCFI0:
    movl    %esp, %ebp     ;set up the base pointer for our parameters and locals
.LCFI1:
    subl    $16, %esp      ;leave room for local variables on the stack
.LCFI2:
    movl    $2, -4(%ebp)   ;store the 2 in "x" (-4 offset from the base pointer)
    movl    -4(%ebp), %edx ;put "x" into the DX register
    movl    8(%ebp), %eax  ;put "a" (+8 offset from base pointer) into AX register
    addl    %edx, %eax     ;add the two together, storing the results in AX
    leave                  ;tear down the stack frame, no more locals or parameters
    ret                    ;exit the function, result is returned in AX by convention
.LFE2:
    .size   _Z6addtwoi, .-_Z6addtwoi
    .ident  "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3"
    .section    .note.GNU-stack,"",@progbits

_Z6addtwoi和.LCFI2之间的所有内容都是样板代码,用于设置堆栈帧(安全地存储先前函数的变量等)。最后一个“subl $16, %esp”是分配本地变量x的操作。
.LCFI2是您键入的第一部分实际执行代码。“movl $2,-4(% ebp)”将值2放入变量中。现在已经分配并初始化了空间。然后它将该值加载到寄存器EDX中,并将参数(在“8(% ebp)”中找到)移动到另一个寄存器EAX中。然后将两者相加,将结果留在EAX中。这是您实际键入的任何代码的结尾。其余部分仍然只是样板。由于GCC要求整数返回在EAX中,因此无需进行任何工作即可获得返回值。 "leave"指令撤销堆栈帧,“ret”指令将控制返回给调用者。
TL;DR摘要:您可以认为您的空间已随着块(配对{})的第一行可执行代码而被分配。

实际上,应该是“变量被定义后立即执行”。声明不会保留任何存储空间:https://dev59.com/V3M_5IYBdhLWcg3wXx1N - sbi
你能在不定义的情况下声明一个本地变量吗?对于全局变量,存储空间在程序启动时分配。 - MSalters
在GCC中,已初始化的全局变量程序启动之前分配。我刚好最近测试过编译一个文件,其中在全局范围内定义了以下内容:static char c[100000000] = "";像这样做的时候,GCC的输出文件会变得非常大。更聪明的编译器如CL将在启动时进行分配和初始化。 - JUST MY correct OPINION

3

关于对象构造:

对象的构造发生在声明点,当对象超出作用域时,析构函数被调用。

但是对象的构造和内存分配不需要同时进行。同样,对象的销毁和内存释放也不需要同时进行。

关于堆栈内存何时真正分配:

我不知道,但您可以通过以下代码进行检查:

void f()
{
  int y;
  y = 0;//breakpoint here

  int x[1000000];
}

通过运行这段代码,您可以看到异常发生的位置,在我的Visual Studio 2008中,它发生在函数进入时,永远不会到达断点。

你的代码并没有展示它实际上是何时分配栈空间的,只有在运行构造函数时才会分配。这两者之间没有必然联系。此外,在那个作用域的结尾处,它并不需要释放 c 的空间,只需要运行析构函数即可。 - Brooks Moses
@Brooks Moses:我觉得在我修改以区分那件事情的时候,您刚好发表了这个评论。 - Brian R. Bondy
是的,看起来是这样,所以我给你点了赞。 :) 我写了一个补充答案,展示了另一种在代码中演示这个的方式。(当然,最好的方法就是查看编译后的汇编代码,看看它在哪里增加了堆栈指针 - 我现在看到有人在做这个评论时也正在这样做;哈哈!) - Brooks Moses
请注意,即使在不进行优化的调试构建中,Linux上的相同代码通常也不会崩溃。除非您使用gcc -fstack-check来使Linux在移动堆栈指针时触摸每个页面,就像在Windows上一样;Linux不需要这样做,但是无论如何都要这样做以加强对堆栈冲突攻击的防御。有关更多信息,请参见Linux进程堆栈被本地变量(堆栈保护)溢出 - Peter Cordes

3
作为Brian R. Bondy答案的补充:可以很容易地运行一些实验来展示这个过程,比抛出堆栈溢出错误更详细。考虑以下代码:
#include<iostream>

void foo()
{
  int e; std::cout << "foo:e " << &e << std::endl;
}

int main()
{
  int a; std::cout << "a: " << &a << std::endl;
  foo();
  int b; std::cout << "b: " << &b << std::endl;
  {
    int c; std::cout << "c: " << &c << std::endl;
    foo();
  }
  int d; std::cout << "d: " << &d << std::endl;
}

这在我的机器上产生了以下输出:
$ ./stack.exe
a: 0x28cd30
foo:e 0x28cd04
b: 0x28cd2c
c: 0x28cd24
foo:e 0x28cd04
d: 0x28cd28

由于堆栈是向下增长的,我们可以看到放入堆栈的顺序:按照a、b、d和c的顺序,然后两次调用foo()将其e放在相同的位置。这意味着每次调用foo()时都分配了相同数量的堆栈内存,即使有几个变量声明(包括一个在内部作用域中声明的变量)。因此,在这种情况下,我们可以得出结论:main()中所有局部变量的堆栈内存都是在main()开始时分配的,而不是逐步增加的。
您还可以看到编译器安排构造函数以降序调用,析构函数以升序调用 - 当它被构造和析构时,它是堆栈底部的已构建东西,但这并不意味着为其分配了底部空间,或者在堆栈上方没有当前未使用的空间,用于尚未构建的事物(例如在构建c或foo:e的两个版本时为d保留的空间)。

@Brooks Moses:我喜欢你的实验设置。在我的一个设置上(i386上的Cygwin,使用g++ 3.4.4),输出略有不同。我得到了相同的地址,分别对应 'c' 和 'd'。 - Arun
谢谢。这是一个有趣的结果——我猜测这是GCC 3.4到4.x之间的差异;我也在i386上使用Cygwin,但是我使用的是g++ 4.3.2编译器。(顺便说一句,这是一个非常推荐的升级,但是你必须在Cygwin安装程序中显式选择gcc4才能获得它。) - Brooks Moses

2

这将取决于编译器,但通常变量int a将在调用函数时分配到堆栈上。


-1,所有编译器在处理局部变量时具有相同的内存分配语义。它们将在声明时分配,并在超出作用域时被释放,就像Brian所说的那样。对于多个局部变量,它们使用LIFO范例进行释放,即最后分配的内存首先被释放。 - YeenFei
3
哇,YeenFei说出的话相当疯狂。“所有编译器在处理本地变量时都具有相同的内存分配语义”。这可能对于某些相对低级的编译器是正确的,但并非所有编译器都如此。许多编译器将为变量分配存储空间,当它第一次被读取或写入时。考虑“int a; int b=2; a=3;”,编译器可以在“b”之后为“a”分配内存。 - Ponkadoodle
2
另外,需要注意的是,这是一种非常简单的分配形式;通常,在作用域内的所有本地变量都从保存在堆栈上的指针得到编译时偏移量,因此当进入该作用域时,程序通过将当前堆栈指针保存在某个位置并逐步增加本地变量的总大小来“分配”这些变量。这比逐步进行更有效,并且意味着您可以使用相同的可变指针和一些常量找到所有本地变量,而不必为每个变量存储不同的可变指针。 - Brooks Moses
可能会有一个C解释器,它在堆上分配所有变量,谁知道呢。 - Grant Paul
YeenFei:这是一个聪明的想法! - Brooks Moses
Brooks:你的例子解决了我在分配和初始化方面的困惑。干得好 :) - YeenFei

2

通常情况下,它处于两者之间。当您调用函数时,编译器将为函数调用生成代码,以评估参数(如果有的话)并将它们放入寄存器或堆栈中。然后,在执行到达函数的入口时,将在堆栈上分配本地变量的空间,并初始化需要初始化的本地变量。此时,可能会有一些代码来保存函数使用的寄存器,将值重新排列以将它们放入所需的寄存器等。在此之后,函数主体的代码开始执行。


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