什么是堆栈展开?
什么是堆栈展开? 通过search,但没有find启发的答案!
关于exception处理,堆栈展开通常是讨论的。 这是一个例子:
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
内存将会丢失,如果抛出exception,分配给内存的内存在任何情况下都会被std::string
析构函数正确释放。 当范围退出时(这里范围是函数func
),堆栈中分配的对象是“unwound”。这是通过编译器将调用插入到自动(堆栈)variables的析构函数中完成的。
现在这是一个非常强大的概念,导致被称为RAII的技术,即资源获取是初始化 ,帮助我们pipe理资源,如内存,数据库连接,打开文件描述符等在C ++中。
现在,我们可以提供例外安全保证 。
所有这些涉及到C ++:
定义 :当静态创build对象(在堆栈上而不是在堆内存中分配它们)并执行函数调用时,它们是“堆叠起来”的。
当范围(由{
和}
分隔的任何东西)退出(通过使用return XXX;
到达范围的末端或抛出exception)时,该范围内的所有内容都被销毁(所有东西都被调用析构函数)。 销毁本地对象和调用析构函数的过程称为堆栈展开。 (使用goto
退出代码块不会解开堆栈,这是你不应该在C ++中使用goto
的原因之一)。
您有以下有关堆栈展开的问题:
-
避免内存泄漏(任何dynamic分配,不由本地对象pipe理,并在析构函数中清理的内容将被泄露) – 请参阅Nikolai 引用的 RAII以及boost :: scoped_ptr的文档或使用boost :: mutex的示例:: scoped_lock 。
-
程序一致性:C ++规范规定,在处理任何现有exception之前,不应抛出exception。 这意味着堆栈展开过程不应该抛出exception (或者只使用保证不会抛出析构函数的代码,或者使用
try {
和} catch(...) {}
)来包围析构函数中的所有内容)。
如果任何析构函数在堆栈展开期间抛出一个exception,那么最终会出现在未定义的行为之中 ,这可能会导致程序出乎意料地(最常见的行为)或宇宙结束(理论上可行,但实际上尚未观察到)。
在一般意义上,“放松”堆栈与函数调用结束和随后的堆栈popup非常相似。
但是,特别是在C ++的情况下,堆栈展开与C ++如何调用自任何代码块启动以来分配的对象的析构函数有关。 块中创build的对象按其分配的相反顺序解除分配。
堆栈展开是一个主要的C ++概念,它处理堆栈分配的对象在其作用域退出时(正常情况下还是exception情况)如何被销毁。
假设你有这段代码:
void hw() { string hello("Hello, "); string world("world!\n"); cout << hello << world; } // at this point, "world" is destroyed, followed by "hello"
我不知道你是否读了这个,但维基百科的调用堆栈文章有一个体面的解释。
平仓:
从被调用的函数返回会popup堆栈顶部框架,也许留下一个返回值。 在堆栈中popup一个或多个框架以在程序中的其他地方恢复执行的更一般行为称为堆栈展开,并且在使用非本地控制结构(例如用于exception处理的结构)时必须执行。 在这种情况下,函数的堆栈框架包含一个或多个指定exception处理程序的条目。 当抛出一个exception时,堆栈被解除展开,直到find一个处理程序准备处理(捕获)抛出exception的types。
一些语言有其他的控制结构,需要一般的展开。 Pascal允许全局goto语句将嵌套函数中的控制权转移到先前调用的外部函数中。 这个操作需要解开堆栈,根据需要移除尽可能多的堆栈帧,以恢复正确的上下文以将控制权转移给封闭外部函数中的目标语句。 同样,C具有setjmp和longjmp函数,可以作为非本地的gotos。 Common Lisp允许通过使用展开保护特殊操作符来控制堆栈解开时发生的情况。
当应用延续时,堆栈(逻辑上)展开,然后与延续堆栈重新缠绕。 这不是执行延续的唯一方法; 例如,使用多个显式堆栈,连续的应用可以简单地激活堆栈并传递一个值。 Scheme编程语言允许在调用continuation时,在控制堆栈的“展开”或“倒带”的指定点执行任意的thunks。
检查[编辑]
我读了一篇帮助我理解的博客文章。
什么是堆栈展开?
在任何支持recursion函数的语言(比如Fortran 77和Brainf * ck)中,语言运行库都保留了当前正在执行的函数的堆栈。 堆栈展开是一种检查和可能修改堆栈的方法。
你为什么要这么做?
答案似乎是显而易见的,但有几个相关的,但又微妙不同的情况,放卷是有用的或必要的:
- 作为运行时控制stream机制(C ++exception,C longjmp()等)。
- 在debugging器中,向用户显示堆栈。
- 在剖析器中,取一个栈的样本。
- 从程序本身(如从崩溃处理程序显示堆栈)。
这些要求有微妙的差别。 其中一些对性能至关重要,有些则不是。 有些需要能够从外部框架重build寄存器,有些则不需要。 但是我们会在一秒钟内完成所有的工作。
你可以在这里find完整的文章。
大家都在谈论C ++中的exception处理。 但是,我认为堆栈展开还有另外一个内涵,那就是与debugging有关。 debugging器必须在任何时候都应该进行堆栈展开,直到它应该转到当前帧之前的一个帧。 但是,这是一种虚拟展开,因为当它返回到当前帧时需要倒回。 这个例子可能是gdb中的up / down / bt命令。
C ++运行时会破坏在throw和catch之间创build的所有自动variables。 在下面这个简单的例子中,f1()throws和main()捕获,在B和Atypes的对象之间按照这个顺序在堆栈上创build。 当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()popup时,自动variablesb被销毁,然后当f()被popup时,自动variablesa被销毁。
希望这有助于,快乐的编码!
当引发exception并且控制从try块传递给处理程序时,C ++运行时将调用自try块开始以来构造的所有自动对象的析构函数。 这个过程被称为堆栈展开。 自动对象以与其构造相反的顺序被销毁。 (自动对象是已经被声明为自动或者注册的本地对象,或者没有被声明为静态或者外部的。当程序退出声明x的块时,自动对象x被删除。
如果在构造由子对象或数组元素构成的对象期间抛出exception,则只会在抛出exception之前调用那些成功构造的子对象或数组元素的析构函数。 只有当对象被成功构造时,才会调用本地静态对象的析构函数。
在Java堆栈解开或解绕不是很重要(用垃圾回收器)。 在许多exception处理文件中,我看到了这个概念(堆栈展开),特别是那些处理C或C ++exception处理的作者。 与try catch
块我们不会忘记: 从本地块后的所有对象的免费堆栈 。
国际海事组织,在这篇文章给出下面的图精美地解释了堆栈展开对下一条指令的路线的影响(一旦抛出一个未被捕获的exception将被执行):
在图片中:
- 最重要的是一个正常的调用执行(不会抛出exception)。
- 底下的一个例外是抛出。
在第二种情况下,当发生exception时,函数调用堆栈会线性searchexception处理程序。 search结束于具有exception处理函数的函数,即main()
与try-catch
块封闭, 但在从函数调用堆栈中删除之前的所有条目之前不进行search 。