Answers:
通常将堆栈展开与异常处理联系在一起讨论。这是一个例子:
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 ++中管理诸如内存,数据库连接,打开文件描述符等资源。
现在,这使我们能够提供例外安全保证。
delete [] pleak;
仅在x == 0时达到
所有这些都与C ++有关:
定义:当您静态创建对象(在堆栈上而不是在堆内存中分配对象)并执行函数调用时,它们被“堆叠”。
退出范围(由{
和界定的任何内容}
)(通过使用return XXX;
,到达范围的末尾或引发异常)时,该范围内的所有内容均被销毁(所有内容都调用了析构函数)。销毁本地对象并调用析构函数的过程称为堆栈展开。
您有以下与堆栈展开有关的问题:
避免内存泄漏(任何不由本地对象管理并在析构函数中清除的动态分配的内容都将泄漏)-请参阅Nikolai 引用的 RAII ,以及boost :: scoped_ptr的文档或使用boost :: mutex的示例:: scoped_lock。
程序一致性:C ++规范规定,在处理任何现有异常之前,切勿引发异常。这意味着,在栈展开过程中不应该抛出异常(或者只使用保证不会在析构函数抛出,或者在析构函数环绕一切与代码try {
和} catch(...) {}
)。
如果有任何析构函数在堆栈展开期间引发异常,则您将陷入未定义行为的境地,这可能导致您的程序意外终止(最常见的行为)或Universe终止(从理论上讲可能,但实际上尚未观察到)。
从一般意义上讲,栈“展开”与函数调用的结束以及随后栈的弹出几乎是同义词。
但是,特别是在C ++的情况下,堆栈展开与自任何代码块启动以来C ++如何为分配的对象调用析构函数有关。在块中创建的对象将按照其分配的相反顺序进行释放。
try
块没有什么特别的。在任何块中分配的堆栈对象(无论是否分配try
)在该块退出时都会展开。
堆栈展开主要是C ++概念,处理退出其作用域(正常或通过异常)时如何销毁堆栈分配的对象。
假设您有以下代码片段:
void hw() {
string hello("Hello, ");
string world("world!\n");
cout << hello << world;
} // at this point, "world" is destroyed, followed by "hello"
我不知道您是否读过这篇文章,但是Wikipedia在调用堆栈上的文章有不错的解释。
展开:
从被调用函数返回将使顶部框架弹出堆栈,可能会保留返回值。从堆栈弹出一个或多个帧以在程序中的其他位置恢复执行的更一般的操作称为堆栈展开并且必须在使用非本地控制结构(例如用于异常处理的结构)时执行。在这种情况下,函数的堆栈框架包含一个或多个指定异常处理程序的条目。引发异常时,将取消堆栈堆栈,直到找到准备处理(捕获)所引发异常类型的处理程序为止。
某些语言具有其他需要常规展开的控制结构。Pascal允许全局goto语句将控制权从嵌套函数转移到先前调用的外部函数中。此操作需要解开堆栈,并根据需要删除尽可能多的堆栈帧,以恢复适当的上下文,从而将控制权转移到封闭的外部函数中的目标语句。同样,C具有setjmp和longjmp函数,它们充当非本地getos。Common Lisp允许使用unwind-protect特殊运算符控制将堆栈展开时发生的情况。
应用延续时,将栈(在逻辑上)解绕,然后与延续的栈重绕。这不是实现延续的唯一方法。例如,使用多个显式堆栈,继续的应用可以简单地激活其堆栈并缠绕要传递的值。Scheme编程语言允许在调用连续性时在控制堆栈的“展开”或“倒带”时在指定点执行任意重击。
检验[编辑]
我读了一篇有助于我理解的博客文章。
什么是堆栈展开?
在任何支持递归函数的语言(即,除了Fortran 77和Brainf * ck之外,几乎所有其他语言)中,语言运行时都会保留当前正在执行的函数的堆栈。堆栈展开是检查并可能修改该堆栈的一种方式。
你为什么想这么做?
答案似乎很明显,但是有几种相关的但又略有不同的情况是有用的或必要的放松:
- 作为运行时控制流机制(C ++异常,C longjmp()等)。
- 在调试器中,向用户显示堆栈。
- 在探查器中,获取堆栈样本。
- 从程序本身(例如从崩溃处理程序显示堆栈)。
这些有不同的要求。其中一些对性能至关重要,而有些则不是。有些要求具有从外部框架重建寄存器的能力,有些则不需要。但是,我们将在一秒钟内处理所有这些。
您可以在此处找到完整的帖子。
每个人都谈论过C ++中的异常处理。但是,我认为堆栈展开还有另一个内涵,它与调试有关。每当调试器应该转到当前帧之前的帧时,它都必须进行堆栈展开。但是,这是一种虚拟展开,因为它需要在回到当前帧时进行回绕。此示例可能是gdb中的up / down / bt命令。
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被销毁。
希望对您有所帮助,编码愉快!