栈展开(stack-unwinding)
抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw
本身是否在try
块内部,如果是,检查与该try
相关的catch
子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch
。这个过程称为栈展开。
有两种情况下会调用析构函数:
1.在正常情况下删除一个对象,例如对象超出了作用域或被显式地delete
2.异常传递的堆栈辗转开解(stack-unwinding)过程
中,由异常处理系统删除一个对象。
在上述两种情况下,调用析构函数时异常可能处于激活状态也可能没有处于激活状态。遗憾的是没有办法在析构函数内部区分出这两种情况。
如果在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用terminate
函数,它终止你程序的运行,而且是立即终止,甚至连局部对象都没有被释放。
考虑以下的类:
class Session
{
public:
Session();
~Session();
...
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
};
函数 logCreation
和 logDestruction
被分别用于记录对象的建立与释放。 我们因此可以这样编写 Session
的析构函数:
Session::~Session()
{
logDestruction(this);
}
如果 logDestruction
抛出一个异常,会发生什么事呢?
异常没有被 Session
的析构函数捕获住,所以它被传递到析构函数的调用者那里。但是如果析构函数本身的调用就是源自于某些其它异常的抛出,那么 terminate
函数将被自动调用,彻底终止你的程序。
…
那么事态果真严重到了必须终止程序运行的地步了么?如果没有,你必须防止在 logDestruction
内抛出的异常传递到 Session
析构函数的外面。唯一的方法是用try
和 catch
blocks。一种很自然的做法会这样编写函数:
Session::~Session()
{
try
{
logDestruction(this);
}
catch (...)
{
cerr << "Unable to log destruction of Session object "
<< "at address "
<< this
<< ".\n";
}
}
但是这样做并不比你原来的代码安全。如果在 catch
中调用 operator<<
时导致一个异常被抛出,我们就又遇到了老问题,一个异常被转递到 Session
析构函数的外面
我们可以在 catch 中放入 try,但是这总得有一个限度,否则会陷入循环。因此我们在释放 Session
时必须忽略掉所有它抛出的异常:
Session::~Session()
{
try
{
logDestruction(this);
}
catch (...) { }
}
catch 表面上好像没有做任何事情,这是一个假象,实际上它阻止了任何从logDestruction
抛出的异常被传递到 session
析构函数的外面。
我们现在能高枕无忧了,无论 session
对象是不是在堆栈退栈(stack unwinding)中被释放,terminate
函数都不会被调用。
…
不允许异常传递到析构函数外面还有第二个原因:
如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数就不会完全运行。
如果析构函数不完全运行,它就无法完成希望它做的所有事情。
例如,我们对 session
类做一个修改,在建立 session
时启动一个数据库事务(database transaction),终止 session
时结束这个事务:
Session::Session() // 为了简单起见,,
{ // 这个构造函数没有
// 处理异常
logCreation(this);
startTransaction(); // 启动 database transaction
}
Session::~Session()
{
logDestruction(this);
endTransaction(); // 结束 database transaction
}
如果在这里 logDestruction
抛出一个异常,在 session
构造函数内启动的 transaction
就没有被终止。我们也许能够通过重新调整 session
析构函数内的函数调用顺序来消除问题,但是如果 endTransaction
也抛出一个异常,我们除了回到使用 try
和 catch
外,别无选择。
总结
禁止异常传递到析构函数外,两个原因:
1.能够在异常转递的堆栈辗转开解(stack-unwinding)
的过程中,防止 terminate
被调用。
2.帮助确保析构函数总能完成我们希望它做的所有事情