什么是堆栈展开?


193

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


76
如果他不知道这是什么,您怎么能期望他知道它们对于C和C ++是不同的?
dreamlax 2010年

@dreamlax:那么,C和C ++中“堆栈展开”的概念有何不同?
毁灭者

2
@PravasiMeet:C没有异常处理,因此堆栈展开非常简单明了,但是,在C ++中,如果引发异常或函数退出,则堆栈展开会破坏具有自动存储持续时间的所有C ++对象。
dreamlax

Answers:


150

通常将堆栈展开与异常处理联系在一起讨论。这是一个例子:

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;
}

这里分配的内存pleak,如果抛出一个异常将会丢失,而内存分配s会得到妥善的释放std::string析构函数在任何情况下。当退出作用域时(这里是作用域的作用域),在堆栈上分配的对象将被“取消缠绕” func。这是通过编译器插入对自动(堆栈)变量的析构函数的调用来完成的。

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

现在,这使我们能够提供例外安全保证


真是令人启发!所以我明白了:如果我的进程在离开任何一个弹出堆栈的块的过程中意外崩溃,则可能发生异常处理程序代码之后的代码根本无法执行,并且可能导致内存泄漏,堆损坏等
Rajendra Uppal 2010年

15
如果程序“崩溃”(即由于错误而终止),则由于内存在终止时释放,因此任何内存泄漏或堆损坏均无关紧要。
Tyler McHenry'2

1
究竟。谢谢。我今天有点阅读困难。
Nikolai Fetissov

11
@TylerMcHenry:该标准不保证在终止时释放资源或内存。然而,大多数操作系统都恰好这样做。
Mooing Duck

3
delete [] pleak;仅在x == 0时达到
。– Jib

71

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

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

退出范围(由{和界定的任何内容})(通过使用return XXX;,到达范围的末尾或引发异常)时,该范围内的所有内容均被销毁(所有内容都调用了析构函数)。销毁本地对象并调用析构函数的过程称为堆栈展开。

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

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

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

如果有任何析构函数在堆栈展开期间引发异常,则您将陷入未定义行为的境地,这可能导致您的程序意外终止(最常见的行为)或Universe终止(从理论上讲可能,但实际上尚未观察到)。


2
反之。尽管不应该滥用gotos,但它们确实会导致MSVC(而不是GCC,因此可能是扩展)中的堆栈展开。setjmp和longjmp以跨平台的方式执行此操作,灵活性稍差一些。
Patrick Niedzielski 2010年

10
我刚刚用gcc进行了测试,当您退出代码块时,它确实可以正确调用析构函数。请参阅stackoverflow.com/questions/334780/…-如该链接所述,这也是标准的一部分。
达米扬

1
按此顺序阅读Nikolai,jrista和您的答案,现在很有意义!
n611x007 2012年

@sashoalm您真的认为七年后需要编辑帖子吗?
大卫·霍尔泽

41

从一般意义上讲,栈“展开”与函数调用的结束以及随后栈的弹出几乎是同义词。

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


4
try块没有什么特别的。在任何块中分配的堆栈对象(无论是否分配try)在该块退出时都会展开。
克里斯·杰斯特·杨

自从我做了很多C ++编码以来已经有一段时间了。我不得不从生锈的深度中挖掘出这个答案。; P
jrista 2010年

不用担心 每个人偶尔都有“他们的坏”。
bitc

13

堆栈展开主要是C ++概念,处理退出其作用域(正常或通过异常)时如何销毁堆栈分配的对象。

假设您有以下代码片段:

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

这适用于任何块吗?我的意思是如果只有{//一些本地对象}
Rajendra Uppal 2010年

@Rajendra:是的,一个匿名块定义了一个范围区域,因此也很重要。
迈克尔·迈尔斯

12

我不知道您是否读过这篇文章,但是Wikipedia在调用堆栈上的文章有不错的解释。

展开:

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

某些语言具有其他需要常规展开的控制结构。Pascal允许全局goto语句将控制权从嵌套函数转移到先前调用的外部函数中。此操作需要解开堆栈,并根据需要删除尽可能多的堆栈帧,以恢复适当的上下文,从而将控制权转移到封闭的外部函数中的目标语句。同样,C具有setjmp和longjmp函数,它们充当非本地getos。Common Lisp允许使用unwind-protect特殊运算符控制将堆栈展开时发生的情况。

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

检验[编辑]


9

我读了一篇有助于我理解的博客文章。

什么是堆栈展开?

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

你为什么想这么做?

答案似乎很明显,但是有几种相关的但又略有不同的情况是有用的或必要的放松:

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

这些有不同的要求。其中一些对性能至关重要,而有些则不是。有些要求具有从外部框架重建寄存器的能力,有些则不需要。但是,我们将在一秒钟内处理所有这些。

您可以在此处找到完整的帖子。


7

每个人都谈论过C ++中的异常处理。但是,我认为堆栈展开还有另一个内涵,它与调试有关。每当调试器应该转到当前帧之前的帧时,它都必须进行堆栈展开。但是,这是一种虚拟展开,因为它需要在回到当前帧时进行回绕。此示例可能是gdb中的up / down / bt命令。


5
调试器动作通常称为“堆栈遍历”,它只是解析堆栈。“堆栈展开”不仅意味着“堆栈漫游”,还意味着调用存在于堆栈中的对象的析构函数。
2013年

@Adisak我不知道它也称为“堆栈行走”。在所有调试器文章的上下文中,甚至在gdb代码内部,我一直都看到“堆栈展开”。我觉得“堆栈展开”更合适,因为它不仅涉及每个功能的堆栈信息,还涉及展开帧信息(比照CFI in dwarf)。这是逐个处理一个功能。
bbv

我猜想Windows会使“堆栈行走”更为著名。另外,我发现code.google.com/p/google-breakpad/wiki/StackWalking作为示例,与矮人标准的文档本身分开使用了几次术语。尽管同意,但这实际上是放松。而且,这个问题似乎在询问“堆栈展开”所暗示的所有可能含义。
bbv

7

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

在此处输入图片说明

在图片中:

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

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


图表很好,但是解释有点混乱。 ...带有封闭的try-catch块,但在从函数调用堆栈中删除之前的所有条目之前都没有...
Atul

3

C ++运行时会破坏在throw&catch之间创建的所有自动变量。在下面的这个简单示例中,在堆栈中按此顺序创建类型为B和A的对象之间的f1()引发和main()捕捉。当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;
}

该程序的输出将是

B's dtor
A's dtor

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

f1()
f()
main()

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

希望对您有所帮助,编码愉快!


2

当引发异常并将控制权从try块传递到处理程序时,C ++运行时将为自try块开始以来构造的所有自动对象调用析构函数。此过程称为堆栈展开。自动对象以相反的顺序销毁。(自动对象是已声明为auto或register或未声明为static或extern的本地对象。只要程序退出声明x的块,就会删除自动对象x。)

如果在构造由子对象或数组元素组成的对象期间抛出异常,则仅在抛出异常之前为成功构造的那些子对象或数组元素调用析构函数。仅当成功构造对象后,才会调用本地静态对象的析构函数。


您应该提供到原始文章的链接,您在其中复制了以下答案:IBM知识库-堆栈
展开-w128

0

在Java堆栈中,展开或展开不是很重要(使用垃圾收集器)。在许多异常处理论文中,我都看到了这个概念(堆栈展开),特别是那些编写器处理C或C ++中的异常处理。对于try catch块,我们应该忘记:在本地块之后释放所有对象的堆栈

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.