本文章摘自:http://blog.sina.com.cn/s/blog_4400494d0100ebi0.html (题目改了一下)
前两篇文章讨论了对象在构造过程中(构造函数)和运行过程中(成员函数)出现异常时的处理情况,本文将讨论最后一种情况,当异常发生在对象的析构销毁过程中时,又会有什么不同呢?主人公阿愚在此可以非常有把握地告诉大家,这将会有大大的不同,而且处理不善还将会毫不留情地影响到软件系统的可靠性和稳定性,后果非常严重。不危言耸听了,看正文吧!
析构函数在什么时候被调用执行?
对于C++程序员来说,这个问题比较简单,但是比较爱唠叨的阿愚还是建议应该在此再提一提,也算回顾一下C++的知识,而且这将对后面的讨论和理解由一定帮助。先看一个简单的示例吧!如下:
class MyTest_Base
{
public:
virtual ~ MyTest_Base ()
{
cout << "销毁一个MyTest_Base类型的对象"<< endl;
}
};
void main()
{
try
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyTest_Base obj;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}
编译运行上面的程序,从程序的运行结果将会表明对象的析构函数被执行了,但什么时候被执行的呢?按C++标准中规定,对象应该在离开它的作用域时被调用运行。实际上各个厂商的C++编译器也都满足这个要求,拿VC来做个测试验证吧!,下面列出的是刚刚上面的那个小示例程序在调试时拷贝出的相关程序片段。注意其中obj对象将会在离开try block时被编译器插入一段代码,隐式地来调用对象的析构函数。如下:
325: try
326: {
00401311 mov dword ptr [ebp-4],0
327: // 构造一个对象,当obj对象离开这个作用域时析构将会被执行
328: MyTest_Base obj;
00401318 lea ecx,[obj]
0040131B call @ILT+40(MyTest_Base::MyTest_Base) (0040102d)
329:
330: } // 瞧下面,编译器插入一段代码,隐式地来调用对象的析构函数
00401320 lea ecx,[obj]
00401323 call @ILT+15(MyTest_Base::~MyTest_Base) (00401014)
331: catch(...)
00401328 jmp __tryend$_main$1 (00401365)
332: {
333: cout << "unknow exception"<< endl;
0040132A mov esi,esp
0040132C mov eax,[__imp_?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z (0041610c)
00401331 push eax
00401332 mov edi,esp
00401334 push offset string "unknow exception" (0041401c)
00401339 mov ecx,dword ptr [__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (00416124)
0040133F push ecx
00401340 call dword ptr [__imp_??6std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z (004
00401346 add esp,8
00401349 cmp edi,esp
0040134B call _chkesp (004016b2)
00401350 mov ecx,eax
00401352 call dword ptr [__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01
00401358 cmp esi,esp
0040135A call _chkesp (004016b2)
334: }
0040135F mov eax,offset __tryend$_main$1 (00401365)
00401364 ret
335: }
析构函数中抛出的异常
1、仍然是先看示例,如下:
class MyTest_Base
{
public:
virtual ~ MyTest_Base ()
{
cout << "开始准备销毁一个MyTest_Base类型的对象"<< endl;
// 注意:在析构函数中抛出了异常
throw std::exception("在析构函数中故意抛出一个异常,测试!");
}
void Func() throw()
{
throw std::exception("故意抛出一个异常,测试!");
}
void Other() {}
};
void main()
{
try
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyTest_Base obj;
obj.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}
程序运行的结果是:
开始准备销毁一个MyTest_Base类型的对象
在析构函数中故意抛出一个异常,测试!
从上面的程序运行结果来看,并没有什么特别的,在程序中首先是构造一个对象,当这个对象在离开它的作用域时,析构函数被调用,此时析构函数中抛出一个std::exception类型的异常,因此后面的catch(std::exception e)块捕获住这个异常,并打印出异常错误信息。这个过程好像显现出,发生在析构函数中的异常与其它地方发生的异常(如对象的成员函数中)并没有什么太大的不同,除了析构函数是隐式调用的以外,但这也丝毫不会影响到异常处理的机制呀!那究竟区别何在?玄机何在呢?继续往下看吧!
2、在上面的程序基础上做点小的改动,程序代码如下:
void main()
{
try
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyTest_Base obj;
// 下面这条语句是新添加的
// 调用这个成员函数将抛出一个异常
obj.Func();
obj.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}
注意,修改后的程序现在的运行结果:非常的不幸,程序在控制台上打印一条语句后就崩溃了(如果程序是debug版本,会显示一条程序将被终止的断言;如果是release版本,程序会被执行terminate()函数后退出)。在主人公阿愚的机器上运行的debug版本的程序结果如下:
许多朋友对这种结果也许会觉得傻了眼,这简直是莫名奇妙吗?这是谁的错呀!难道是新添加的那条代码的问题,但这完全不会呀!(其实很多程序员朋友受到过太多这种类似的冤枉,例如一个程序原来运行挺好的,以后进行功能扩充后,程序却时常出现崩溃现象。其实有时程序扩充时也没添加多少代码,而且相关程序员也很认真仔细检查自己添加的代码,确认后来添加的代码确实没什么问题呀!可相关的负责人也许不这么认为,觉得程序以前一直运行挺好的,经过你这一番修改之后就出错了,能不是你添加的代码所导致的问题吗?真是程序开发领域的窦娥冤呀!其实这种推理完全是没有根据和理由的,客观公正一点地说,程序的崩溃与后来添加的模块代码肯定是会有一定的相关性!但真正的bug也许就在原来的系统中一直存在,只不过以前一直没诱发表现出来而已!瞧瞧!主人公阿愚又岔题了,有感而发!还是回归正题吧!)
那究竟是什么地方的问题呢?其实这实际上由于析构函数中抛出的异常所导致的,但这就更诧异了,析构函数中抛出的异常是没有问题的呀!刚才的一个例子不是已经测试过了吗?是的,但那只是一种假象。如果要想使你的系统可靠、安全、长时间运行无故障,你在进行程序的异常处理设计和编码过程中,至少要保证一点,那就是析构函数中是不永许抛出异常的,而且在C++标准中也特别声明到了这一点,但它并没有阐述真正的原因。那么到底是为什么呢?为什么C++标准就规定析构函数中不能抛出异常?这确实是一个非常棘手的问题,很难阐述得十分清楚。不过主人公阿愚还是愿意向大家论述一下它自己对这个问题的理解和想法,希望能够与程序员朋友们达成一些理解上的共识。
C++异常处理模型是为C++语言量身设计的,更进一步的说,它实际上也是为C++语言中面向对象而服务的,我们在前面的文章中多次不厌其烦的声明到,C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。好的,既然如此!那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源,这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。不知大家是否明白了这段话所蕴含的真正内在涵义没有,那就是上面的论述C++异常处理模型它其实是有一个前提假设——析构函数中是不应该再有异常抛出的。试想!如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。所以C++标准就做出了这种假设,当然这种假设也是完全合理的,在对象的构造过程中,或许由于系统资源有限而致使对象需要的资源无法得到满足,从而导致异常的出现,但析构函数完全是可以做得到避免异常的发生,毕竟你是在释放资源呀!好比你在与公司续签合同的时候向公司申请加薪,也许公司由于种种其它原因而无法满足你的要求;但如果你主动申请不要薪水完全义务工作,公司能不乐意地答应你吗?
假如无法保证在析构函数中不发生异常,怎么办?
虽然C++标准中假定了析构函数中不应该,也不永许抛出异常的。但有过的实际的软件开发的程序员朋友们中也许会体会到,C++标准中的这种假定完全是站着讲话不觉得腰痛,实际的软件系统开发中是很难保证到这一点的。所有的析构函数的执行过程完全不发生一点异常,这根本就是天方夜谭,或者说自己欺骗自己算了。而且大家是否还有过这种体会,有时候发现析构一个对象(释放资源)比构造一个对象还更容易发生异常,例如一个表示引用记数的句柄不小心出错,结果导致资源重复释放而发生异常,当然这种错误大多时候是由于程序员所设计的算法在逻辑上有些小问题所导致的,但不要忘记现在的系统非常复杂,不可能保证所有的程序员写出的程序完全没有bug。因此杜绝在析构函数中决不发生任何异常的这种保证确实是有点理想化了。那么当无法保证在析构函数中不发生异常时,该怎么办?我们不能眼睁睁地看着系统崩溃呀!
其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。按这种方法把上面的程序再做一点改动,那么程序将避免了崩溃的厄运。如下:
class MyTest_Base
{
public:
virtual ~ MyTest_Base ()
{
cout << "开始准备销毁一个MyTest_Base类型的对象"<< endl;
// 一点小的改动。把异常完全封装在析构函数内部
try
{
// 注意:在析构函数中抛出了异常
throw std::exception("在析构函数中故意抛出一个异常,测试!");
}
catch(…) {}
}
void Func() throw()
{
throw std::exception("故意抛出一个异常,测试!");
}
void Other() {}
};
程序运行的结果如下:
开始准备销毁一个MyTest_Base类型的对象
故意抛出一个异常,测试!
怎么样,现在是不是一切都已经风平浪静了。
析构函数中抛出异常时概括性总结
(1) C++中析构函数的执行不应该抛出异常;
(2) 假如析构函数中抛出了异常,那么你的系统将变得非常危险,也许很长时间什么错误也不会发生;但也许你的系统有时就会莫名奇妙地崩溃而退出了,而且什么迹象也没有,崩得你满地找牙也很难发现问题究竟出现在什么地方;
(3) 当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外(这招简直是绝杀!呵呵!);
(4) 主人公阿愚吐血地提醒朋友们,一定要切记上面这几条总结,析构函数中抛出异常导致程序不明原因的崩溃是许多系统的致命内伤!
至此在C++程序中,各种可能的地方抛出的异常将如何地来处理这个主题已基本讨论完毕!从下一篇文章开始继续讨论有关C++异常处理其它方面的一些问题。朋友们,CONTINUE!