在{}作用域中声明变量后,它们是否仍然会占用内存?

8
在这个例子中,即使我永远不会使用变量WNDCLASSEX、x、y、cx、cy,但当我在消息循环中时,它们仍然会占用内存。
int WINAPI WinMain (HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpArgs, int iCmdShow)
    {
     WNDCLASSEX wc;
     ...
     RegisterClassEx(&wc);

     const int cx = 640;
     const int cy = 480; 
     // center of the screen
     int x = (GetSystemMetrics(SM_CXSCREEN) - cx) / 2;
     int y = (GetSystemMetrics(SM_CXSCREEN) - cy) / 2;

     CreateWindow(..., x, y, cx, cy, ...);

     MSG msg;

     while (GetMessage(&msg, NULL, 0, 0) > 0)
     {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
     }
     return 0;
    }

但是我在想,如果我把它们放在作用域中,它们是否仍然会在消息循环期间使用内存?例如:

int WINAPI WinMain (HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpArgs, int iCmdShow)
{
 {
  WNDCLASSEX wc;
  ...
  RegisterClassEx(&wc);

  const int cx = 640;
  const int cy = 480; 
  // center of the screen
  int x = (GetSystemMetrics(SM_CXSCREEN) - cx) / 2;
  int y = (GetSystemMetrics(SM_CXSCREEN) - cy) / 2;

  CreateWindow(..., x, y, cx, cy, ...);
 }

 MSG msg;

 while (GetMessage(&msg, NULL, 0, 0) > 0)
 {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }
 return 0;
}

也许如果我将它们放入两个函数中,并在winmain中调用它们,例如:
wnd_register(hInst);
wnd_create(hInst);

那会阻止他们使用内存吗?


虽然像nightcracker所说的那样测试起来很容易。你花在提问上的时间可能比测试自己的代码还要多!好问题 +1 - Nick Rolando
1
为什么要声明你不使用的变量? - Falmarri
这真的是一个无用的微优化。在Windows上,默认情况下线程有1 MB的堆栈。你担心浪费不到可用堆栈空间的百分之一。 - Michael
我没有测试,因为我认为如果编译器看到我在那个作用域之后没有使用它(即使我可以),它会将其优化掉。 - Kaije
9个回答

6
编译器在处理像您示例中的简单局部变量时有很大的灵活性。它们可以存在于堆栈上,也可以仅存在于机器代码中作为立即值,或者只存在于寄存器中。堆栈空间通常在进入函数时分配。编译器将从堆栈指针中减去一些值以为所有本地变量腾出空间。在函数返回时,堆栈指针将恢复到其原始值。这通常不会在不同作用域块退出时执行。大多数编译器将尝试在变量不再使用时立即重用堆栈空间。在您的示例中,x和msg具有完全相同的堆栈地址是完全合法的,因为它们的使用是非重叠的。
我的this question的答案更详细地介绍了如何在堆栈上分配本地变量。
在您的示例中,常量cx和cy很可能在运行时没有内存支持,只是生成代码中的立即值。x和y很可能会存储在寄存器中,直到需要将它们推送到堆栈以调用CreateWindow。wc和msg几乎肯定在堆栈上。
在这个层面上不应该担心微小的优化 - 让编译器按照自己的方式分配局部变量的空间。您默认有1MB的堆栈,这些变量消耗的数据量甚至不会被视为噪音。花时间去考虑更有趣的问题。

+1,但实际上编译器可能无法重叠wc和x,因为wc的地址被取出并传递给函数,所以它可能在稍后的调用中仍然处于活动状态,例如CreateWindow。我说“可能”是因为编译器可以想象进行跨函数分析,但大多数编译器不会这样做。 - Chris Dodd
没错,获取地址确实可以防止编译器重用空间。由于RegisterClassEx和CreateWindow位于不同的模块中,编译器无法进行跨函数分析。 - Michael

3
也许不会,但这是实现细节。它们已经被销毁了(如果有析构函数调用的话)。标准并没有规定系统何时以及何时回收自动存储的内存。据我所知,大多数情况下会立即归还。

任何在类对象内部使用的动态内存,例如字符串,都应该立即由对象的析构函数释放。我想知道是否有编译器为了提高速度而保留堆栈内存,直到函数结束,即使通常只需要1个指令来释放它? - Mark Ransom
1
大多数编译器会积极地重用堆栈空间。在他的示例中,msg和wc具有完全相同的地址是完全合法的。 - Michael
@Michael,我可以理解第二种情况是可能的,但对于第一种情况,编译器无法在函数结束之前调用wc的析构函数,因此它无法与任何东西共享内存。 - Mark Ransom
WNDCLASSEX是一个简单的结构体,没有析构函数。即使它有析构函数,只要该析构函数不涉及该内存,编译器就可以重用该内存。 - Michael
另外,我想强调的是,编译器这样做是合法的,但并不是所有优化编译器都一定会执行我所述的优化。 - Michael
1
@Michael,我忘了POD的情况——如果编译器知道对象没有析构函数,并且在看到变量在块结束前没有使用时就预先查看了它,那么它肯定可以自由地重用它。你提到的优化是可选的,这一点很有道理,但正如你之前暗示的那样,大多数编译器都会利用这个优化。如果对象有内联析构函数,编译器可以对其进行一些分析,以确保没有副作用,但这有点棘手。 - Mark Ransom

1

嗯,我不确定它们是否使用内存或标准对此有何规定。

我知道的是,在内存块 { } 的结尾处,析构函数将被调用并且变量将无法访问。这意味着,虽然它没有被释放,但至少可以重复使用。

例如:

struct Foo {
    Foo(void) { std::cout << "Hi!"; }
    ~Foo(void) { std::cout << "Bye!"; }
};

int main(int argc, char * argv[])
{
    {
        Foo bar; // <- Prints Hi!
    } // <- Prints Bye!

    // Memory used by bar is now available.
}

编辑:感谢Tomalak Geret'kal ;)


这意味着,虽然它没有被释放,但至少可以被重复使用。你无法控制自动变量的存储位置。因此,尚未归还的内存是无法“重复使用”的。 - Edward Strange
请注意,由于您有一个运行代码的显式构造函数,编译器必须在块退出时运行它。这并不意味着底层堆栈空间发生任何变化。 - Michael
@Noah,从堆栈中弹出一个变量并将另一个变量推回去本质上是一个NOP操作。为什么编译器不会简单地重用内存并在原地构造新对象呢?可能无法控制自动变量的位置,但编译器肯定可以。 - Mark Ransom
@Mark - 可能是这样。但实现可以选择不同的方式。事实上,它甚至不必使用堆栈。我们知道在C++中会发生的事情是析构函数被调用并且名称变得不可用。其余部分取决于实现,当然会做一些合理的事情使其有用,但实现可能采用的技巧可能违反我们可能想要给出的任何通用、常识性答案,因此这样的答案是不可取的,可能是错误的。 - Edward Strange

1
一个神奇的建议:相信你的编译器。它会进行优化,而且非常聪明,比我们大多数人都要优化得更好。
如果你不确定,可以使用分析器或者在编译器优化后检查汇编输出。但是请记住,微小的优化是你不应该在代码中做的,因为这是毫无意义的,只会影响代码的可读性。
一些变量(特别是常量)将不会在堆栈上使用任何内存,因为它们将被映射到CPU寄存器或直接嵌入到汇编指令中。
这意味着以下代码:
func(123+456*198*value);

并且

int a = 123;
int b = 56;
int c = 400;
int d = b+c;
int e = d*198;
e *= value;
e += a;
func(e);

如果变量不再使用,编译后的结果将完全相同。

说真的,别费心了。如果你想优化,从算法角度来优化,而不是从语法角度。


0

哦天啊,程序运行时内存中有四个整数,真是浪费!

  1. 试一下吧,一个简单的消息框尝试打印它们应该就可以了(我想)。
  2. 别管它。

实际上,WndClassEx 有大约10个成员,包括一个字符串。 - Kaije
连这个都做不到。你会得到一个编译错误,'x' 未声明。 - Nick Rolando
wndclass 中的字符串很可能是一个常量,并且不会从本地堆栈中出来。 - Michael

0

你在 {} 中声明的变量将会超出作用域并且丢失。实际上,如果你试图在代码块外使用它们,你会得到一个编译错误:'x' 未声明。然而,这样做是不规范的。正如你在编辑中所说,只需为此代码编写一个函数即可。保持你的 main() 尽可能少的行数是良好的编程实践。


0

它们不会。它们只会在其封闭块的结尾处停止运行。


0

如果您将它们放在函数的嵌套作用域内(第一种选择),那么当控制流到达作用域的末尾时,变量将变得不可访问(如果直接使用它们,则会出现编译错误;如果保存其中一个指针,则会出现运行时未定义的行为),它们的析构函数将被运行(如果有析构函数),实现可能会在堆栈帧中重新使用它们的存储空间。但标准并不要求重用这个空间。

如果您将函数分成两部分(第二种选择)...就标准而言,没有区别!当函数返回时,变量变得不可访问,它们的析构函数被运行,实现可能会重用它们的存储空间,但不是必须的。并且已经有严肃的实现——虽然不是C/C++的实现——不立即回收该内存:最著名的是论文“Cheney on the M.T.A.

然而,就我目前所知,所有的C/C++实现都会在函数返回时回收分配给函数局部变量的内存。对于嵌套局部作用域变量的内存回收则不太确定。无论如何,正如其他几个人提到的那样,在这种情况下,担心几十字节的堆栈空间是不值得的。

就个人而言,我会将您的代码分成两个函数,因为这样每个函数只执行一个任务。从长远维护的角度来看,这通常更好。


0

通常情况下,如果变量在堆栈上,则在整个封闭函数的持续时间内将在堆栈上占用其空间。编译器通常会计算函数变量可能占用的最大空间,然后在首次进入函数时使函数一次性分配所有空间。但是,在内部作用域的进入和退出时仍将调用构造函数和析构函数。一个作用域中的变量空间可能被重用以表示来自另一个作用域的变量。


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