C++ 函数调用与堆栈上推送/弹出的新块

Posted

技术标签:

【中文标题】C++ 函数调用与堆栈上推送/弹出的新块【英文标题】:C++ Function Call vs. New Blocks for Push/Popping on the Stack 【发布时间】:2011-08-22 02:06:53 【问题描述】:

我在阅读 C++ 中的变量作用域时遇到了一个有趣的块结构:

int main(int argc, char **argv) 
    int local;

     // New level of scope
        int more_local;
    

    return 0;

我知道变量会在每个块的末尾从堆栈中弹出,由右花括号 表示。

我还读到函数调用也会将它们的变量压入堆栈并在调用结束时终止,用大括号 表示:

void foo() 
    int more_local;


int main(int argc, char **argv) 
    int local;
    foo();

    return 0;

在这两种情况下,堆栈的处理方式有何不同,两者的优缺点是什么?

【问题讨论】:

【参考方案1】:

通过函数调用,您将返回地址压入堆栈并创建一个新的堆栈帧。如您所说,如果您只是将部分代码括在花括号中,那么您正在定义一个新的范围。它们就像 if、for、while 等控制语句之后的任何代码块。

你不能在这里真正谈论优点和缺点,因为这是完全不同的两件事。将代码块括在花括号中的情况并不多,这会使代码更难阅读。

【讨论】:

如果我编译上面的两个示例,我会假设具有 new scope 的示例将与函数调用相比进行微优化,因为与创建相关的额外开销一个新的堆栈帧。这不是优势吗?我的逻辑可能有一些错误,但似乎存在性能差异(无论多么小)。 当然,如果您的目标只是创建一个新范围,那么使用花括号。但你为什么要这样做?如果要重用变量名,我会警告不要这样做。如果要尝试优化您的内存使用,我不会尝试像那样对其进行微观管理;编译器应该能够对其进行足够好的优化。 @Gio:根据优化级别(/O1 应该已经足够了),该函数调用很可能会被内联,具体取决于其实际长度和内容。【参考方案2】:

好吧,您可以说您的第一个示例可以看作是一个内联函数。 :P 但是一般来说,函数调用和打开一个新的scope是没有关系的。 当你调用一个函数时,返回地址和所有参数都被压入堆栈,并在函数返回后从堆栈中弹出。 当打开一个新的scope 时,您只需在其末尾调用该范围内所有对象的析构函数;绝不保证这些变量占用的实际空间会立即从堆栈中弹出。它可以,但空间也可以简单地被函数中的其他变量重用,这取决于编译器/优化器的奇思妙想。

【讨论】:

我认为你的意思是构造函数,而不是析构函数,因为析构函数在作用域的末尾被调用。 我刚读到内联函数:“当编译器内联扩展函数调用时,函数的代码会插入到调用者的代码流中”如果我内联 第二个例子的功能,和第一个例子有什么不同吗? 小心,编译器内联扩展函数调用,而不是你。即使您使用 inline 关键字,它也只是对编译器的提示。该函数可能在编译期间内联,也可能不内联,具体取决于编译器最终决定的内容。【参考方案3】:

int more_local; 在这两种情况下都将被放入堆栈。但是第二种情况会产生函数调用的开销。

我建议你考虑一下:

void foo()

    int local;

     // New level of scope
        int more_local_1;
    
     // New level of scope
        int more_local_2;
    

这里more_local_1more_local_2 可能共享相同的内存位置。一旦用于 more_local_1more_local_2 变量的第二个作用域。

【讨论】:

【参考方案4】:

局部作用域仍然可以访问其他局部变量,而函数必须显式传递它们需要使用的任何调用者变量

传递变量是一种痛苦,但有时它使代码更易于理解,以清楚地指示作用域操作实际需要的较小变量集(以及鼓励将操作分组为离散的功能单元,给定适当的函数名称和与上下文相关的参数名称,然后就可以重复使用了)

离线函数调用还有一些其他的堆栈空间和性能开销:返回地址、保存的寄存器、调用和返回指令

与函数作用域相比,局部作用域特别适用于最小化持有重要资源的变量的作用域,例如大量内存、线程、文件描述符和/或锁:更高级别且运行时间更长的函数及时清理可能更有用

减少的变量生命周期还减少了程序员必须在精神上“跟踪”以理解和维护代码的并发变量的数量:越少越好

有时在执行一组类似操作时必须选择任意不同的标识符没有多大意义,因此一些本地范围允许标识符方便地“回收”

本地范围有点笨拙,并且在源代码中占用“屏幕空间”,并且会增加缩进级别,因此最好在有特定理由时使用它们而不是在“只要你可以” "基础

【讨论】:

【参考方案5】:

如果您观察这两个程序的汇编代码,似乎没有区别,因为编译器在遇到左大括号或函数调用并弹出时似乎生成汇编代码以在当前堆栈指针处推送新堆栈帧一旦遇到右花括号或 return 语句,框架就会退出。第二种情况的优点是您可以使用 return 语句将值返回给调用者函数。但不是第一种情况。

【讨论】:

以上是关于C++ 函数调用与堆栈上推送/弹出的新块的主要内容,如果未能解决你的问题,请参考以下文章

切换 C++ 函数的调用堆栈

区分导致视图控制器被推送的用户操作

调用堆栈上大量对象的构造函数

如何在单个函数调用中处理视图的连续推送/弹出

C++ 堆栈分配对象,显式析构函数调用

cadel fastcall stdcall