Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3518575
  • 博文数量: 864
  • 博客积分: 14125
  • 博客等级: 上将
  • 技术积分: 10634
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-27 16:53
个人简介

https://github.com/zytc2009/BigTeam_learning

文章分类

全部博文(864)

文章存档

2023年(1)

2021年(1)

2019年(3)

2018年(1)

2017年(10)

2015年(3)

2014年(8)

2013年(3)

2012年(69)

2011年(103)

2010年(357)

2009年(283)

2008年(22)

分类: C/C++

2010-05-05 19:02:43

第6集 对象的成员函数中抛出的异常
--------------------------------------------------------------------------------

CSAI.CN原创 王胜祥 2005年05月16日
  C++异常处理模型除了支持面向过程的C风格程序中的异常处理外(就是没有面向对象的概念,完全是C程序,整个程序实际就是函数的集合,但却用C++编译器来编译这样的C程序,所以这样的程序中是可以a使用C++的异常处理机制的,要不怎么说C++是兼容C语言的呢?但是需要注意的是,单纯的C语言程序中是不能使用C++异常处理模型进行编程的。是不是有点说拗口了?有点糊涂了呢?其实很简单,那就是如果程序中使用了C++异常处理机制,也即代码中有try、catch和throw关键字,那么就必须使用C++编译器来编译这个程序。许多程序员朋友们在这里有一个理解上的误区,认为只有程序中使用了面向对象的概念,即使用class关键字来定义一个类结构才算得上C++程序,其实这种理解是片面的,如果程序中采用了C++异常处理机制,那么也有理由认为这是一个C++程序,哪怕程序的代码完全是C语言风格的,并且这样的程序用C编译器来编译肯定将会报错,提示未定义的try标示符等等错误信息),还支持面向对象程序中对象抛出的异常处理。

  C++异常处理模型的确和面向对象是紧密结合的,除了在相遇篇中介绍到的用对象来描述程序中出现的异常之外,C++异常处理模型也对在面向对象程序中的对象实例所抛出的异常作了最完善的支持和处理。也许大家会觉得这很容易,没什么了不起的地方。但恰恰相反,实际上这才是C++异常处理模型最成功、最不可思议和最闪光的地方。而且由于C++异常处理模型对面向对象有了很好的支持和兼容,才使得C++异常处理模型本身的实现变得特别复杂,因为它需要跟踪每一个对象的运行情况和状态(关于C++异常处理模型的实现,会在爱的秘密篇中有详细剖析)。本文和接下来的几篇文章将讲述当对象实例抛出异常时将如何处理。

  对象的生命周期一般有三种状态:构造、运行和析构销毁。因此对象抛出的异常也有这三种区别。是在对象构造时抛出的呢?还是对象运行时抛出的呢?或是析构对象时抛出的?这三种不同时候抛出的异常会将会产生不同的结果。本文首先讨论最常见的一种情况,在对象运行时抛出的异常,也即执行对象的成员函数时出现的异常。

对象的成员函数抛出的异常

  1、老方法,看例子先,如下:

class MyTest_Base
{
public:
MyTest_Base (string name = “”) : m_name(name)
{
cout << “构造一个MyTest_Base类型的对象,对象名为:”<}

virtual ~ MyTest_Base ()
{
cout << “销毁一个MyTest_Base类型的对象,对象名为:”<}

void Func() throw()
{
throw std::exception(“故意抛出一个异常,测试!”);
}
void Other() {}

protected:
string m_name;
};

void main()
{
try
{
MyTest_Base obj1(“obj1”);

// 调用这个成员函数将抛出一个异常,注意obj1的析构函数会被执行吗?如果
// 会,又是在什么时候被执行呢?
obj1.Func();
obj1.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

  C++程序员不难看出上面的程序的运行结果,如下:
  构造一个MyTest_Base类型的对象,对象名为:obj1
  销毁一个MyTest_Base类型的对象,对象名为:obj1
  故意抛出一个异常,测试!

  从运行结果可以得出如下结论:
  (1) 对象的成员函数出现异常时,catch block能捕获到异常,这一点就像C语言中的普通函数一样,没什么特别的地方;
  (2) 对象的成员函数出现异常时,对象的析构函数将会得到执行(这一点很神奇吧!当然在这里不会做过多研究,在剖析C++异常处理模型的实现时再做详细的阐述),这里与C++标准中规定的面向对象的特性是相一致的,构造了的对象就必须保证在适当的地方要析构它,以释放可能的资源。因此前面说的“C++异常处理模型对面向对象提供了支持和兼容”是有根据的。而且注意它的析构函数是在异常处理模块之前执行的,这一点更与C++标准中规定的面向对象的特性是一致的,当对象出了作用域时,它就必须要被析构。

  2、把上面的程序小改一下,运行再看结果,如下:

void main()
{
// obj1对象不在trycatch域中,注意它的析构函数在什么时候被执行?
MyTest_Base obj1(“obj1”);
try
{
// obj2和obj3对象都在trycatch域中,其中obj3.Func()函数被调用,因此
// obj3会抛出异常,特别需要注意的是,obj2的析构函数会被执行吗?如果
// 会,又是在什么时候被执行呢?
MyTest_Base obj2(“obj2”), obj3(“obj3”);

obj3.Other();

// 调用这个成员函数将抛出一个异常
obj3.Func();

// 注意:obj4对象在构造之前,函数中就有异常抛出。所以obj4对象将不会
// 被构造,当然也不会被析构
MyTest_Base obj4(“obj4”);
obj3.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

  上面的程序也难看出其运行结果,如下:
  构造一个MyTest_Base类型的对象,对象名为:obj1
  构造一个MyTest_Base类型的对象,对象名为:obj2
  构造一个MyTest_Base类型的对象,对象名为:obj3
  销毁一个MyTest_Base类型的对象,对象名为:obj3
  销毁一个MyTest_Base类型的对象,对象名为:obj2
  故意抛出一个异常,测试!
  销毁一个MyTest_Base类型的对象,对象名为:obj1

  结合程序中提出的问题和运行结果,可以又可得出如下结论:
  (1) 在成员函数出现异常时,同一个作用域中异常出现点后面还未来得及构造的对象将不会被构造,当然也不会被析构;
  (2) 在成员函数出现异常时,同一个作用域中异常出现点前面已经构造的对象也同样会被析构(这是不是更神奇了!)。因此这也显现出C++异常处理不会破坏C++标准中规定的面向对象的特性,当对象出了作用域时,它就必须要被析构,即便它自己本身没出现异常,总之不管是正常的执行过程导致对象退出了作用域,还是其它对象运行时发生了异常而导致自己退出了作用域;
  (3) 在成员函数出现异常时,未被影响到的其它作用域中的对象将保持自己原来的执行流程。

对象的成员函数抛出的异常时概括性总结

   哈哈^-^,其是就只有一句话,那就是“C++的异常处理不会破坏任何一条面向对象的特性!”,因此主人公阿愚建议大家其实无须要记住上面总结的n条结论,记住这一条足矣!

  下篇文章讨论在构造函数中抛出异常时程序的执行情况,这有点复杂呀!朋友们,Let's go!
第7集 构造函数中抛出的异常
--------------------------------------------------------------------------------

CSAI.CN原创 王胜祥 2005年05月16日
  上一篇文章简单讨论了一下对象的成员函数抛出异常时的处理情况。本文中将继续讨论当在构造函数中抛出异常时,程序的执行情况又如何?这有点复杂呀!而且主人公阿愚还觉得这蛮有点意思!

构造函数中抛出的异常

  1、标准C++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证对象的构造过程一定能成功呢?说不定系统当前的某个资源不够,导致对象不能完全构建好自己(人都有畸形儿,更何况别的呢?朋友们!是吧!),因此通过什么方法来表明对象的构造失败了呢?C++程序员朋友们知道,C++中的构造函数是没有返回值的,所以不少关于C++编程方面的书上得出结论:“因为构造函数没有返回值,所以通知对象的构造失败的唯一方法那就是在构造函数中抛出异常”。主人公阿愚非常不同意这种说法,谁说的,便不信邪!虽然C++标准规定构造函数是没有返回值,可我们知道每个函数实际上都会有一个返回值的,这个值被保存在eax寄存器中,因此实际上是有办法通过编程来实现构造函数返回一个值给上层的对象创建者。当然即便是构造函数真的不能有返回值,我们也可以通过一个指针类型或引用类型的出参来获知对象的构造过程的状态。示例如下:

class MyTest_Base
{
public:
MyTest_Base (int& status)
{
//do other job

// 由于资源不够,对象构建失败
// 把status置0,通知对象的构建者
status = 0;
}

protected:
};

void main()
{
int status;
MyTest_Base obj1(status);

// 检查对象的构建是否成功
if(status ==0) cout << “对象构建失败” << endl;
}

  程序运行的结果是:
  对象构建失败
  是啊!上面我们不也得到了对象构造的成功与否的信息了吗?可大家有没有觉得这当中有点问题?主人公阿愚建议大家在此停留片刻,仔细想想它会有什么问题?OK!也许大家都知道了问题的所在,来验证一下吧!

class MyTest_Base
{
public:
MyTest_Base (int& status)
{
//do other job

// 由于资源不够,对象构建失败
// 把status置0,通知对象的构建者
status = 0;
}

virtual ~ MyTest_Base ()
{
cout << “销毁一个MyTest_Base类型的对象” << endl;
}


protected:
};

void main()
{
int status;
MyTest_Base obj1(status);

// 检查对象的构建是否成功
if(status ==0) cout << “对象构建失败” << endl;
}

  程序运行的结果是:
  对象构建失败
  销毁一个MyTest_Base类型的对象

  没错,对象的析构函数被运行了,这与C++标准中所规定的面向对象的一些特性是有冲突的。一个对象都没有完成自己的构造,又何来析构!好比一个夭折的畸形儿还没有出生,又何来死之言。因此这种方法是行不通的。那怎么办?那就是上面那个结论中的后一句话是对的,通知对象的构造失败的唯一方法那就是在构造函数中抛出异常,但原因却不是由于构造函数没有返回值而造成的。恰恰相反,C++标准中规定构造函数没有返回值正是由于担心很容易与面向对象的一些特性相冲突,因此干脆来个规定,构造函数不能有返回值(主人公阿愚的个人理解,有不同意见的朋友欢迎讨论)。

  2、构造函数中抛出异常将导致对象的析构函数不被执行。哈哈^-^,阿愚很开心,瞧瞧!如果没有C++的异常处理机制鼎立支持,C++中的面向对象特性都无法真正实现起来,C++标准总不能规定所有的对象都必须成功构造吧!这也太理想化了,也许只有等到共产主义社会实现的那一天(CPU可以随便拿,内存可以随便拿,所有的资源都是你的!)才说不定有可能·····,所以说C++的异常处理和面向对象确实是谁也离不开谁。当然示例还是要看一下,如下:

class MyTest_Base
{
public:
MyTest_Base (string name = “”) : m_name(name)
{
throw std::exception(“在构造函数中抛出一个异常,测试!”);
cout << “构造一个MyTest_Base类型的对象,对象名为:”<}

virtual ~ MyTest_Base ()
{
cout << “销毁一个MyTest_Base类型的对象,对象名为:”<}

void Func() throw()
{
throw std::exception(“故意抛出一个异常,测试!”);
}
void Other() {}

protected:
string m_name;
};

void main()
{
try
{
// 对象构造时将会抛出异常
MyTest_Base obj1(“obj1”);

obj1.Func();
obj1.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

  程序的运行结果将会验证:“构造函数中抛出异常将导致对象的析构函数不被执行”

  3、是不是到此,关于构造函数中抛出异常的处理的有关讨论就能结束了呢?非也!非也!主人公阿愚还有进一步的故事需要讲述!来看一个更复杂一点的例子吧!如下:

class MyTest_Base
{
public:
MyTest_Base (string name = "") : m_name(name)
{
cout << "构造一个MyTest_Base类型的对象,对象名为:"<}

virtual ~ MyTest_Base ()
{
cout << "销毁一个MyTest_Base类型的对象,对象名为:"<}

void Func() throw()
{
throw std::exception("故意抛出一个异常,测试!");
}
void Other() {}

protected:
string m_name;
};

class MyTest_Parts
{
public:
MyTest_Parts ()
{
cout << "构造一个MyTest_Parts类型的对象" << endl;
}

virtual ~ MyTest_Parts ()
{
cout << "销毁一个MyTest_Parts类型的对象"<< endl;
}
};

class MyTest_Derive : public MyTest_Base
{
public:
MyTest_Derive (string name = "") : m_component(), MyTest_Base(name)
{
throw std::exception("在MyTest_Derive对象的构造函数中抛出了一个异常!");

cout << "构造一个MyTest_Derive类型的对象,对象名为:"<}

virtual ~ MyTest_Derive ()
{
cout << "销毁一个MyTest_Derive类型的对象,对象名为:"<}

protected:
MyTest_Parts m_component;
};

void main()
{
try
{
// 对象构造时将会抛出异常
MyTest_Derive obj1("obj1");

obj1.Func();
obj1.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}

  程序运行的结果是:
  构造一个MyTest_Base类型的对象,对象名为:obj1
  构造一个MyTest_Parts类型的对象
  销毁一个MyTest_Parts类型的对象
  销毁一个MyTest_Base类型的对象,对象名为:obj1
  在MyTest_Derive对象的构造函数中抛出了一个异常!

  上面这个例子中,MyTest_Derive从MyTest_Base继承,同时MyTest_Derive还有一个MyTest_Parts类型的成员变量。现在MyTest_Derive构造的时候,是在父类MyTest_Base已构造完毕和MyTest_Parts类型的成员变量m_component也已构造完毕之后,再抛出了一个异常,这种情况称为对象的部分构造。是的,这种情况很常见,对象总是由不断的继承或不断的聚合而来,对象的构造过程实际上是这些所有的子对象按规定顺序的构造过程,其中这些过程中的任何一个子对象在构造时发生异常,对象都不能说自己完成了全部的构造过程,因此这里就有一个棘手的问题,当发生对象的部分构造时,对象将析构吗?如果时,又将如何析构呢?

  从运行结果可以得出如下结论:
  (1) 对象的部分构造是很常见的,异常的发生点也完全是随机的,程序员要谨慎处理这种情况;
  (2) 当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构(即异常发生点前面的对象);而还没有开始构建的子对象将不会被构造了(即异常发生点后面的对象),当然它也就没有析构过程了;还有正在构建的子对象和对象自己本身将停止继续构建(即出现异常的对象),并且它的析构是不会被执行的。

  构造函数中抛出异常时概括性总结
  (1) C++中通知对象构造失败的唯一方法那就是在构造函数中抛出异常;
  (2) 构造函数中抛出异常将导致对象的析构函数不被执行;
  (3) 当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构;
  (4) 哈哈^-^,其是还是那句话, “C++的异常处理不会破坏任何一条面向对象的特性!”,因此主人公阿愚再次建议朋友们,牢牢记住这一条!

  下一篇文章讨论在对象的析构函数中抛出异常时程序的执行情况,这不仅有些复杂,而且很关键,它对我们的软件系统影响简直太大了,可许多人并未意识到这个问题的严重性!朋友们,不要错过下一篇文章,继续吧!
第8集 析构函数中抛出的异常
--------------------------------------------------------------------------------

CSAI.CN原创 王胜祥 2005年05月16日
  前两篇文章讨论了对象在构造过程中(构造函数)和运行过程中(成员函数)出现异常时的处理情况,本文将讨论最后一种情况,当异常发生在对象的析构销毁过程中时,又会有什么不同呢?主人公阿愚在此可以非常有把握地告诉大家,这将会有大大的不同,而且处理不善还将会毫不留情地影响到软件系统的可靠性和稳定性,后果非常严重。不危言耸听了,看正文吧!

析构函数在什么时候被调用执行?

  对于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
阅读(737) | 评论(0) | 转发(0) |
0

上一篇:C++异常(2)

下一篇:C++异常(4)

给主人留下些什么吧!~~