什么是栈展开?

什么是栈展开?搜索了一遍,但没有找到有启发性的答案!

112430 次浏览

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

解除:

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

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

当应用一个continuation时,堆栈(逻辑上)被展开,然后用该continuation的堆栈重新展开。这不是实现延续的唯一方法;例如,使用多个显式堆栈,continuation的应用程序可以简单地激活它的堆栈并wind要传递的值。Scheme编程语言允许在“unwind”上的指定点上执行任意的坦克。或“;rewinding"调用延续时,控件堆栈的。

检查[编辑]

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

假设你有这样一段代码:

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

在一般意义上,堆栈“unwind”几乎等同于函数调用的结束和随后的堆栈弹出。

然而,特别是在c++的情况下,堆栈展开必须与c++如何调用自任何代码块开始分配的对象的析构函数有关。在块中创建的对象将按照其分配的相反顺序被释放。

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

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++中管理内存、数据库连接、打开的文件描述符等资源。

现在这允许我们提供异常安全保证

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

< p > 定义: 当你静态地创建对象(在堆栈上,而不是在堆内存中分配它们)并执行函数调用时,它们是“堆叠起来的”

当一个作用域(由{}分隔的任何内容)退出(通过使用return XXX;,到达作用域的末尾或抛出异常)时,该作用域内的所有内容都被销毁(对所有内容调用析构函数)。这个销毁局部对象并调用析构函数的过程称为堆栈展开。

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

  1. 避免内存泄漏(任何不受本地对象管理并在析构函数中清理的动态分配都会泄漏)-参见RAII 引用 by Nikolai,和boost:: scoped_ptr或本例使用boost::mutex::scoped_lock

  2. 程序一致性:c++规范规定,在处理任何现有异常之前,永远不要抛出异常。这意味着堆栈展开过程不应该抛出异常(要么只使用保证不抛出析构函数的代码,要么用try {} catch(...) {}包围析构函数中的所有内容)。

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

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

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

c++运行时销毁在throw &之间创建的所有自动变量;抓住。在下面这个简单的例子中,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;
}

这个程序的输出将是

B's dtor
A's dtor

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

f1()
f()
main()

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

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

我读了一篇博客文章,帮助我理解。

什么是栈展开?

在任何语言中,支持递归函数(即。差不多 除了Fortran 77和Brainf*ck),语言运行时保持不变 当前正在执行的函数的堆栈。堆栈展开是 一种检查和修改堆栈的方法

你为什么要这么做?

答案似乎是显而易见的,但有几个相关的,但很微妙 不同的情况下,展开是有用的或必要的:

  1. 作为运行时控制流机制(c++异常,C longjmp()等)。
  2. 在调试器中,向用户显示堆栈。
  3. 在分析器中,获取堆栈的示例。
  4. 从程序本身(比如从崩溃处理程序显示堆栈)。
它们的要求略有不同。 有些是性能关键型的,有些不是。有些要求 能够从外部帧重构寄存器,有些不。但 我们马上就会讲到。

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

在我看来,这个文章中给出的下图漂亮地解释了堆栈展开对下一条指令路径的影响(在抛出未捕获的异常时执行):

enter image description here

在图片中:

  • 上面一个是正常的调用执行(没有抛出异常)。
  • 当抛出异常时,位于底部。

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

堆栈展开是在运行时从函数调用堆栈中删除函数项的过程。它通常与异常处理有关。在c++中,当发生异常时,函数调用堆栈会线性地搜索异常处理程序,在带有异常处理程序的函数之前的所有条目都将从函数调用堆栈中删除。