c++构造函数和析构函数抛出异常的注意事项
原文转载处
从语法上来说,构造函数和析构函数都可以抛出异常。但从逻辑上和风险控制上,构造函数和析构函数中尽量不要抛出异常,万不得已,一定要注意防止资源泄露。在析构函数中抛出异常还要注意栈展开带来的程序崩溃。
1.构造函数中抛出异常
在C++构造函数中,既需要分配内存,又需要抛出异常时要特别注意防止内存泄露的情况发生。因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用。同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行的结束,并释放已经构造的成员对象,当然包括其基类的成员,即要执行直接基类和成员对象的析构函数。考察如下程序。
#include <iostream>
using namespace std;
class C{
int m;
public:
C(){cout<<"in C constructor"<<endl;}
~C(){cout<<"in C destructor"<<endl;}
};
class A{
public:
A(){cout<<"in A constructor"<<endl;}
~A(){cout<<"in A destructor"<<endl;}
};
class B:public A{
public:
C c;
char* resource;
B(){
resource=new char[100];
cout<<"in B constructor"<<endl;
throw -1;
}
~B(){
cout<<"in B destructor"<<endl;
delete[] resource;
}
};
int main(){
try{
B b;
}
catch(int){
cout<<"catched"<<endl;
}
}
程序输出结果:
in A constructor
in C constructor
in B constructor
in C destructor
in A destructor
catched
从输出结果可以看出,在构造函数中抛出异常,当前对象的析构函数不会被调用,如果在构造函数中分配了内存,那么就会造成内存泄露,所以要格外注意。
此外,在构造函数B的对象b的时候,先要执行其直接基类A的构造函数,再执行其成员对象c的构造函数,然后再进入类B的构造函数。由于在类B的构造函数中抛出了异常,而此异常并未在构造函数中被捕捉,所以导致类B的构造函数的执行中断,对象b并未构造完成。在类B的构造函数“回滚”的过程中,c的析构函数和类A的析构函数相继被调用。最后,由于b并没有被成功构造,所以main()函数结束时,并不会调用b的析构函数,也就很容易造成内存泄露。
2.析构函数中抛出异常
在析构函数中是可以抛出异常的,但是这样做很危险,请尽量不要这要做。原因在《More Effective C++》中提到两个:
(1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
(2)通常异常发生时,c++的异常处理机制在异常的传播过程中会进行栈展开(stack-unwinding),因发生异常而逐步退出复合语句和函数定义的过程,被称为栈展开。在栈展开的过程中就会调用已经在栈构造好的对象的析构函数来释放资源,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。
那么如果无法保证在析构函数中不发生异常, 该怎么办?
其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出析构函数之外。这是一种非常简单,也非常有效的方法。
~ClassName()
{
try{
do_something();
}
catch(…){ //这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外
}
}
在面对析构函数中抛出异常时,程序猿要注意以下几点:
(1)C++中析构函数的执行不应该抛出异常;
(2)假如析构函数中抛出了异常,那么你的系统将变得非常危险,也许很长时间什么错误也不会发生;但也许你的系统有时就会莫名奇妙地崩溃而退出了,而且什么迹象也没有,不利于系统的错误排查;
(3)当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外。
一定要切记上面这几条总结,析构函数中抛出异常导致程序不明原因的崩溃是许多系统的致命内伤!