(本节内容可参照Effective C++ 9共同阅读)
前言
析构函数只有2种情况会被调用:
- 正常情况,对象离开作用域或被显式delete
- 对象被异常处理机制——异常传播过程中的stack-unwinding机制析构
在这两种情况中,调用析构函数时异常可能处于激活状态也可能没有被激活(无法在析构函数内进行区分),我们只能假定异常处于激活状态。因为在一个异常被激活的同时,如果析构函数再次抛出异常,C++会立刻终止程序运行,甚至连局部变量都尚未释放。
问题实例
有一个session类用来跟踪在线计算机的sessions,session是计算机从登陆到注销时一直存在的某种文件。每个session对象关注的是它建立与释放的日期与时间:1
2
3
4
5
6
7
8
9class Session {
public:
Session();
~Session();
...
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
};
函数logCreation与logDestruction被分别用于记录对象的建立与释放。我们因此可以这样编写 Session 的析构函数:1
2
3Session::~Session(){
logDestruction(this);
}
如果logDestruction抛出异常,会发生什么?
实例剖析
析构函数抛出的异常并没有被捕获,因此它会被传递到调用位置。但是如果析构函数本身的调用就是源自于某些其它异常的抛出,那么terminate函数将被自动调用,彻底终止程序。为了保证logDestruction抛出的异常被传递到析构函数之外,我们必须手动地加以try-catch:1
2
3
4
5
6
7
8Session::~Session(){
try {
logDestruction(this);
}
catch (...) {
cerr << "Unable to log destruction of Session object ";
}
}
看起来异常确实被捕获了,但如果operator << 抛出了一个异常….程序又会被直接终止运行。
因此,最终解决方案是释放Session时忽略掉所有它抛出的异常:1
2
3
4
5
6Session::~Session(){
try {
logDestruction(this);
}
catch (...) { }
}
catch语句看起来像什么都没做,但他从实际上保证了异常不会被传递到析构函数之外,因此terminate不会被调用。
非完全析构
除却保证程序不会因为多个异常同时激活导致termi,异常不得离开析构函数的第二个原因就是保证完全析构。如果一个异常被析构函数抛出但却没有在函数内部被捕获,那么析构函数就不会完全运行(停在抛出异常的那个地方)。如果析构函数不完全运行,就会发生非完全析构。
仍以session为例,假设在建立session时启动一个database transaction,析构session时结束它:1
2
3
4
5
6
7
8Session::Session() {
logCreation(this);
startTransaction();
}
Session::~Session(){
logDestruction(this);
endTransaction();
}
显然,如果logDestruction抛出一个异常,transaction就没有被终止。
总结
禁止异常传递到析构函数之外有两个原因:
- 阻止terminate被调用
- 阻止非完全析构