什么是堆栈展开?

Raj*_*pal 186 c++ stack

什么是堆栈展开?通过搜索但找不到启发性的答案!

Nik*_*sov 143

堆栈展开通常与异常处理有关.这是一个例子:

void func( int x )
{
    char* pleak = new char[1024]; // might be lost => memory leak
    std::string s( "hello world" ); // will be properly destructed

    if ( x ) throw std::runtime_error( "boom" );

    delete [] pleak; // will only get here if x == 0. if x!=0, throw exception
}

int main()
{
    try
    {
        func( 10 );
    }
    catch ( const std::exception& e )
    {
        return 1;
    }

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这里分配的内存pleak,如果抛出一个异常,将会丢失,而内存分配给s将被正确地释放std::string析构函数在任何情况下.当退出作用域时,在堆栈上分配的对象是"展开的"(这里的作用域是函数func.)这是通过编译器插入对自动(堆栈)变量的析构函数的调用来完成的.

现在,这是一个非常强大的概念,导致称为RAII的技术,即资源获取是初始化,它帮助我们在C++中管理内存,数据库连接,打开文件描述符等资源.

现在,这使我们能够提供异常安全保障.

  • 如果程序"崩溃"(即*由于错误而终止*),则任何内存泄漏或堆损坏都是无关紧要的,因为内存在终止时被释放. (13认同)
  • @TylerMcHenry:该标准不保证在终止时释放资源或内存.然而,大多数操作系统都是这样做的. (9认同)
  • `delete [] pleak;`只有在x == 0时才会到达. (3认同)
  • @TylerMcHenry,不完全。该进程可能会泄漏系统/会话全局资源(例如 Windows 上的 ATOM)。 (2认同)

utn*_*tim 68

所有这些都与C++有关:

定义:当您静态创建对象(在堆栈上而不是在堆内存中分配它们)并执行函数调用时,它们会"堆叠".

当一个范围(由{and 分隔的任何东西})退出时(通过使用return XXX;,到达范围的末尾或抛出异常),该范围内的所有内容都将被销毁(析构函数将被调用).这种破坏本地对象和调用析构函数的过程称为堆栈展开.

您有以下与堆栈展开相关的问题:

  1. 避免内存泄漏(由本地对象管理并在析构函数中清理的任何动态分配将被泄露) - 请参阅Nikolai 提到的 RAII ,以及boost :: scoped_ptr的文档或使用boost :: mutex的示例:: scoped_lock.

  2. 程序一致性:C++规范规定在处理任何现有异常之前,不应抛出异常.这意味着,在堆栈展开过程中不应该抛出异常(或者只使用不保证代码析构函数抛出,或环绕一切都在析构函数与try {} catch(...) {}).

如果任何析构函数在堆栈展开期间抛出异常,则最终会出现未定义行为,这可能导致程序意外地(最常见的行为)或宇宙结束(理论上可能但在实践中尚未观察到).

  • 我刚刚使用gcc对它进行了测试,当你转出代码块时它确实正确地调用了析构函数.请参阅http://stackoverflow.com/questions/334780/are-goto-and-destructors-compatible/334781#334781 - 如该链接所述,这也是标准的一部分. (10认同)
  • 反之.虽然不应该滥用gotos,但它们会导致MSVC中的堆栈展开(不是在GCC中,因此它可能是一个扩展).setjmp和longjmp以跨平台方式执行此操作,灵活性稍差. (2认同)

jri*_*sta 41

在一般意义上,堆栈"展开"几乎与函数调用的结束和随后的堆栈弹出同义.

但是,特别是在C++的情况下,堆栈展开与C++如何调用自任何代码块启动以来分配的对象的析构函数有关.在块中创建的对象按其分配的相反顺序解除分配.

  • `try`块没有什么特别之处.在块中退出时,在_any_块中分配的堆栈对象(无论是否为"try")都需要展开. (4认同)

Chr*_*ung 13

堆栈展开主要是C++概念,处理堆栈分配对象在其范围退出时(通常或通过异常)被销毁的情况.

假设你有这段代码:

void hw() {
    string hello("Hello, ");
    string world("world!\n");
    cout << hello << world;
} // at this point, "world" is destroyed, followed by "hello"
Run Code Online (Sandbox Code Playgroud)


Joh*_*don 12

我不知道你是否读过这篇文章,但维基百科关于调用堆栈的文章有一个不错的解释.

平仓:

从被调用函数返回将弹出堆栈的顶部框架,可能留下返回值.从堆栈中弹出一个或多个帧以恢复程序中其他位置执行的更一般的行为称为堆栈展开,并且必须在使用非本地控制结构时执行,例如用于异常处理的结构.在这种情况下,函数的堆栈帧包含一个或多个指定异常处理程序的条目.抛出异常时,堆栈被展开,直到找到准备处理(捕获)抛出异常类型的处理程序.

某些语言具有其他需要一般展开的控制结构.Pascal允许全局goto语句将控制权从嵌套函数转移到先前调用的外部函数中.此操作需要展开堆栈,根据需要删除尽可能多的堆栈帧以恢复正确的上下文,以将控制转移到封闭外部函数内的目标语句.类似地,C具有充当非本地gotos的setjmp和longjmp函数.Common Lisp允许通过使用unwind-protect特殊运算符来控制堆栈展开时发生的情况.

在应用延续时,堆栈(逻辑上)展开,然后用延续的堆栈重绕.这不是实现延续的唯一方法; 例如,使用多个显式堆栈,继续的应用可以简单地激活其堆栈并传递要传递的值.当调用continuation时,Scheme编程语言允许在控制堆栈的"展开"或"倒带"的指定点上执行任意thunks.

检查[编辑]


L. *_*ngó 8

我读了一篇帮助我理解的博客文章.

什么是堆栈展开?

在任何支持递归函数的语言中(例如除了Fortran 77和Brainf*ck之外的几乎所有东西)语言运行时都会保留当前正在执行的函数的堆栈.堆栈展开是一种检查并可能修改堆栈的方法.

你为什么想这么做?

答案可能看起来很明显,但有几个相关但又略有不同的情况,其中展开是有用的或必要的:

  1. 作为运行时控制流机制(C++异常,C longjmp()等).
  2. 在调试器中,向用户显示堆栈.
  3. 在分析器中,获取堆栈的样本.
  4. 从程序本身(如从崩溃处理程序到显示堆栈).

这些要求略有不同.其中一些是性能关键,有些则不是.有些需要能够从外框重建寄存器,有些则不需要.但我们会在一秒钟之内完成所有这些工作.

你可以在这里找到完整的帖子.


bbv*_*bbv 6

每个人都谈到了C++中的异常处理.但是,我认为堆栈展开有另一个含义,这与调试有关.每当调试器应该转到当前帧之前的帧时,调试器就必须进行堆栈展开.但是,这是一种虚拟的展开,因为当它回到当前帧时需要倒带.这个例子可能是gdb中的up/down/bt命令.

  • 调试器操作通常称为"堆栈遍历",它只是解析堆栈."Stack Unwinding"不仅意味着"Stack Walking",还意味着调用堆栈中存在的对象的析构函数. (5认同)

Sau*_*ahu 6

IMO时,这在下面图给出文章精美解释堆栈展开下一条指令的路线上的效果(一旦则抛出异常是未捕获的将被执行):

在此处输入图片说明

在图片中:

  • 最重要的是正常的调用执行(不引发异常)。
  • 引发异常时,倒数第一。

在第二种情况下,当发生异常时,将在函数调用堆栈中线性搜索异常处理程序。搜索在带有异常处理程序(即main()带有封闭try-catch块)的函数处结束,但不是在从函数调用堆栈中删除之前的所有条目之前。


Dig*_*Eye 5

C++ 运行时会破坏在 throw 和 catch 之间创建的所有自动变量。在下面的这个简单示例中,f1() 抛出并 main() 捕获,在类型 B 和 A 的对象之间按该顺序在堆栈上创建。当 f1() 抛出异常时,B 和 A 的析构函数被调用。

#include <iostream>
using namespace std;

class A
{
    public:
       ~A() { cout << "A's dtor" << endl; }
};

class B
{
    public:
       ~B() { cout << "B's dtor" << endl; }
};

void f1()
{
    B b;
    throw (100);
}

void f()
{
    A a;
    f1();
}

int main()
{
    try
    {
        f();
    }
    catch (int num)
    {
        cout << "Caught exception: " << num << endl;
    }

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

该程序的输出将是

B's dtor
A's dtor
Run Code Online (Sandbox Code Playgroud)

这是因为 f1() 抛出时程序的调用堆栈看起来像

f1()
f()
main()
Run Code Online (Sandbox Code Playgroud)

因此,当 f1() 被弹出时,自动变量 b 被销毁,然后当 f() 被弹出时,自动变量 a 被销毁。

希望这有帮助,编码愉快!