一、C++异常分析:
构造函数中抛出异常是有一定必要的,试想如下情况:
构造函数中有两次new操作,第一次成功了,返回了有效的内存,而第二次失败,此时因为对象构造尚未完成,析构函数是不会调用的,也就是delete语句没有被执行,第一次new出的内存就悬在那儿(发生内存泄漏),所以异常处理程序可以将其暴露出来。
//.....
Base()
{
int* p = new int();
try{
int* q = new int(); //假如失败
//throw 2... //如果直接抛异常,构造函数失败,不执行析构函数
}
catch (...){
delete p;
throw;
}
}
构造函数中遇到异常是不会调用析构函数的,一个对象的父对象的构造函数执行完毕,不能称之为构造完成,对象构造是不可分割的,要么完全成功,要么完全失败,C++保证这一点。对于成员变量,C++遵循这样的规则,即会从异常的发生点按照成员变量的初始化的逆序释放成员。举例来说,有如下初始化列表:
A::A():m1(),m2(),m3(),m4(),m5()
{...}
假定m3的初始化过程中抛出异常,则会按照m2,m1的顺序调用这两个成员的析构函数。在{}之间发生的未捕捉异常,最终会导致在栈的开解时析构所有的数据成员。
处理这样的问题,使用智能指针是最好的,这是因为auto_ptr成员是一个对象而不是指针。换句话说,只要不使用原始的指针,那么就不必担心构造函数抛出异常而导致资源泄漏。所以在C++中,资源泄漏的问题一般都用RAII(资源获取就是初始化)的办法:把需要打开/关闭的资源用简单的对象封装起来(这种封装可以同时有多种用处,比如隐藏底层API细节,以利于移植)。
如果不用RAII,即使当前构造函数里获取的东西在析构函数中都释放了,如果某天对类有改动,要新增加一种资源,构造函数里一般能适当地获取,但记不记得在析构函数里相应地释放呢?失误的比率很大。如果考虑到构造函数里抛出异常,就更复杂了。随着项目的不断扩大和时间的推移,这些细节不可能都记得住,而且,有可能会由别人来实施这样的改动。
从运行结果得出以下的结论:
(1)、C++中通知对象构造失败的唯一方法,就是在构造函数中抛出异常;
(2)、对象的部分构造是很常见的,异常的发生点也完全是随机的,程序员要谨慎处理这种情况;
(3)、当对象发生部分构造时,已经构造完成的子对象将会发生逆序地被析构(即异常发生点前面的对象);而还没有开始构建的子对象将不会被 构造了(即异常发生点后面的对象),当然它也就没有析构的过程了;还有正在构建的子对象和对象自己本身就停止继续构建(即出现异常的对象),并且它的析构是不会被执行的。
二、析构函数抛异常的情况:
Effective C++建议,析构函数尽可能地不要抛出异常。设想如果对象出了异常 ,现在异常处理模块为了维护系统数据对象的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且新出现的异常由谁来处理?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说处于无限的递归嵌套中。