什么是堆栈展开?搜索过但无法找到启发性的答案!
答案 0 :(得分:132)
堆栈展开通常与异常处理有关。这是一个例子:
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的技术,即 资源获取初始化 ,这有助于我们管理内存等资源, C ++中的数据库连接,打开文件描述符等。
现在,我们可以提供exception safety guarantees。
答案 1 :(得分:61)
这一切都与C ++有关:
<强>定义强>: 当您静态创建对象(在堆栈上而不是在堆内存中分配它们)并执行函数调用时,它们会“堆叠”。
当一个范围(由{
和}
分隔的任何内容)退出时(通过使用return XXX;
,到达范围的末尾或抛出异常)该范围内的所有内容都将被销毁(所有的东西都需要析构函数)。 这个破坏本地对象和调用析构函数的过程称为堆栈展开。
您有以下与堆栈展开相关的问题:
避免内存泄漏(动态分配的任何不受本地对象管理并在析构函数中清理的内容都会被泄露) - 请参阅Nikolai的RAII referred to和the documentation for boost::scoped_ptr或者此使用boost::mutex::scoped_lock。
程序一致性:C ++规范声明在处理任何现有异常之前不应抛出异常。这意味着堆栈展开过程不应该抛出异常(要么只使用保证不会抛出析构函数的代码,要么使用try {
和} catch(...) {}
包围析构函数中的所有内容。
如果任何析构函数在堆栈展开期间抛出异常,则会导致未定义行为,这可能导致程序意外终止(最常见的行为)或Universe结束(理论上可能但是尚未在实践中观察到。)
答案 2 :(得分:40)
在一般意义上,堆栈“展开”几乎与函数调用的结束和随后的堆栈弹出同义。
但是,特别是在C ++的情况下,堆栈展开与C ++如何调用自任何代码块启动以来分配的对象的析构函数有关。在块中创建的对象按其分配的相反顺序解除分配。
答案 3 :(得分:13)
堆栈展开主要是C ++概念,处理堆栈分配对象在退出其范围时(通常或通过异常)的销毁方式。
假设你有这段代码:
void hw() {
string hello("Hello, ");
string world("world!\n");
cout << hello << world;
} // at this point, "world" is destroyed, followed by "hello"
答案 4 :(得分:11)
我不知道您是否已阅读此内容,但Wikipedia's article on the call stack有一个不错的解释。
<强>放卷:强>
从被调用函数返回将弹出堆栈的顶部框架,可能会留下返回值。从堆栈中弹出一个或多个帧以恢复在程序中其他位置执行的更一般行为称为堆栈展开,并且必须在使用非本地控制结构时执行,例如用于异常的那些处理。在这种情况下,函数的堆栈帧包含一个或多个指定异常处理程序的条目。抛出异常时,堆栈被展开,直到找到准备处理(捕获)抛出异常类型的处理程序。
某些语言具有其他需要一般展开的控制结构。 Pascal允许全局goto语句将控制权从嵌套函数转移到先前调用的外部函数中。此操作需要展开堆栈,根据需要删除尽可能多的堆栈帧以恢复正确的上下文,以将控制转移到封闭外部函数内的目标语句。类似地,C具有充当非本地gotos的setjmp和longjmp函数。 Common Lisp允许通过使用unwind-protect特殊运算符来控制堆栈展开时发生的事情。
当应用延续时,堆栈(逻辑上)展开,然后用延续的堆栈重绕。这不是实现延续的唯一方法;例如,使用多个显式堆栈,继续的应用可以简单地激活其堆栈并传递要传递的值。 Scheme调度语言允许在调用continuation时在控件堆栈的“展开”或“倒带”中的指定点执行任意thunk。
检验[编辑]
答案 5 :(得分:8)
我读了一篇帮助我理解的博客文章。
什么是堆栈展开?
支持递归函数的任何语言(即非常多 除了Fortran 77和Brainf * ck之外的所有内容,语言运行时都会保留 一堆当前正在执行的功能。堆栈展开是 一种检查并可能修改该堆栈的方法。
你为什么要这样做?
答案似乎显而易见,但有几个相关但却巧妙 不同的,放松是有用或必要的情况:
- 作为运行时控制流机制(C ++异常,C longjmp()等)。
- 在调试器中,向用户显示堆栈。
- 在分析器中,获取堆栈样本。
- 从程序本身(如从崩溃处理程序中显示堆栈)。
醇>这些要求略有不同。 其中一些是性能关键,有些则不是。有些需要 从外框重建寄存器的能力,有些则没有。但 我们将在一秒钟内完成所有这些工作。
您可以找到完整的帖子here。
答案 6 :(得分:6)
每个人都谈到了C ++中的异常处理。但是,我认为堆栈展开有另一个含义,这与调试有关。每当调试器应该转到当前帧之前的帧时,调试器就必须进行堆栈展开。但是,这是一种虚拟的展开,因为当它回到当前帧时需要倒带。这个例子可能是gdb中的up / down / bt命令。
答案 7 :(得分:4)
IMO,这个article中给出的下图清楚地解释了堆栈展开对下一条指令路由的影响(抛出一个未被捕获的异常后执行):
在图片中:
在第二种情况下,当发生异常时,线性搜索函数调用堆栈以寻找异常处理程序。搜索结束于具有异常处理程序的函数,即main()
,其中包含try-catch
块,但之前从函数调用堆栈中删除所有条目。
答案 8 :(得分:3)
C ++运行时会破坏在throw&amp;之间创建的所有自动变量。抓住。在下面的简单示例中,f1()throws和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;
}
该程序的输出将是
B's dtor
A's dtor
这是因为当f1()抛出时程序的callstack看起来像
f1()
f()
main()
因此,当弹出f1()时,自动变量b被破坏,然后当弹出f()时,自动变量a被破坏。
希望这有帮助,快乐编码!
答案 9 :(得分:2)
当抛出异常并且控制从try块传递到处理程序时,C ++运行时调用自try块开始以来构造的所有自动对象的析构函数。此过程称为堆栈展开。自动对象按其构造的相反顺序销毁。 (自动对象是已声明为auto或register,或未声明为static或extern的本地对象。只要程序退出声明x的块,就会删除自动对象。)
如果在构造由子对象或数组元素组成的对象期间抛出异常,则只会在抛出异常之前为成功构造的子对象或数组元素调用析构函数。仅当成功构造对象时,才会调用本地静态对象的析构函数。
答案 10 :(得分:1)
在Java堆栈中,展开或展开不是很重要(使用垃圾收集器)。在许多异常处理文件中,我看到了这个概念(堆栈展开),特别是那些写作者处理C或C ++中的异常处理。使用try catch
块我们不会忘记:在本地块之后的所有对象中的自由堆栈。
答案 11 :(得分:0)
堆栈展开是在运行时从函数调用堆栈中删除函数条目的过程。它通常与异常处理有关。在 C++ 中,当发生异常时,函数调用堆栈会线性搜索异常处理程序,然后从函数调用堆栈中删除带有异常处理程序的函数之前的所有条目。