登录 用户中心() [退出] 后台管理 注册
   
您的位置: 首页 >> 程序员学前班[不再更新,只读] >> 主题: 异常处理的编程方法 前言 [转]     [回主站]     [分站链接]
标题
异常处理的编程方法 前言 [转]
clq
浏览(0) + 2007-09-10 14:49:58 发表 编辑

关键字:

转自:http://blog.programfan.com/article.asp?id=9737

作者简介:王胜祥,高级程序员,网络工程师,系统分析员。CSAI专业顾问,中国计算机技术与软件专业资格水平考试辅导指定教程系列丛书编委会成员。
  王胜祥曾从事过多个项目的系统分析和设计工作,参与了国家863高新技术研究发展计划的多个课题的研究和开发工作。详细而深入阅读过minix和linux源码,深刻理解现代操作系统的基本原理和类unix系统的实现技巧。全面研究和分析过XFree86的总体架构和实现原理,熟悉X协议。系统地掌握与现代网络相关的纵多技术和网络协议,以及网络的规划和设计。
  王胜祥目前致力于Linux系统下的NC软件产品的研发与应用,以及Linux系统下建立统一组件模型的可行性研究。

主人公介绍

  阿愚,曾经是一位小小程序员,现在仍是一位小小程序员,将来也还是一位小小程序员。读程序、写程序一直就是他的最爱,这过程给他带来许许多多的快乐,虽然期间也有过一些迷茫,但每次冲过迷雾,重见阳光的喜悦总是他对程序人生的更加执著追求。雨过天晴的空气才是最清新的。

  异常处理的编程方法,程序员都很熟悉的一个东东,她和面向对象的方法是软件程序设计发展史上其中最重要的两项革新技术。现代程序设计语言拥有的一个重要的特性就是能较好地支持异常的处理(Exception Handling)。她就像一位美丽而优雅的公主,帮助程序员写出来的代码总是那样的整齐美观、层次清晰;同时它好像还是一位贤惠能干的贤内助,总能帮你料理好由于考虑不全所留下的多多少少的意外事件,她在背后默默的支持你的一切,使你写出来的作品是那样的高效、安全和完美。瞧!它深深地打动了我们我们的主人公阿愚,并续上了一段美丽的编程爱情故事。

内容的组织及编排

相遇篇

《第1集 初次与异常处理编程相邂逅》
《第2集 C++中异常处理的游戏规则》
《第3集 C++中catch(…)如何使用》
《第4集 C++的异常处理和面向对象的紧密关系》
《第5集 C++的异常rethrow》

相知篇

《第6集 对象的成员函数中抛出的异常》
《第7集 构造函数中抛出的异常》
《第8集 析构函数中抛出的异常》
《第9集 C++的异常对象如何传送》
《第10集 C++的异常对象按传值的方式被传递》
《第11集 C++的异常对象按引用方式被传递》
《第12集 C++的异常对象按指针方式被传递》
《第13集 C++异常对象三种方式传递的综合比较》
《第14集 再探C++中异常的rethrow》
《第15集 C语言中的异常处理机制》
《第16集 C语言中一种更优雅的异常处理机制》
《第17集 全面了解setjmp与longjmp的使用》
《第18集 玩转setjmp与longjmp》
《第19集 setjmp与longjmp机制,很难与C++和睦相处》
《第20集 C++中如何兼容并支持C语言中提供的异常处理机制》
《第21集 Windows系列操作系统平台中的提供的异常处理机制》
《第22集 更进一步认识SEH》
《第23集 SEH的强大功能之一》
《第24集 SEH的强大功能之二》
《第25集 SEH的综合》
《第26集 SEH可以在C++程序中使用》
《第27集 SEH与C++异常模型的混合使用》
《第28集 Java中的异常处理模型》
《第29集 Unix操作系统提供中的异常处理机制》

相爱篇

《让异常成为函数接口的一部分》
《异常能够优雅地跨越组件》
《C++标准库中的异常分类模型》
《MFC类库中的异常分类模型》
《JDK平台中的异常分类模型》

爱的秘密

《实现》

爱的结晶

《对现有模型的一些完善与改进》


clq
2007-9-10 14:51:49 发表 编辑


  和其它很多程序员一样,本书的主人公阿愚也是在初学C++时,在C++的sample代码中与异常处理的编程方法初次邂逅的,如下:

// Normal program statements
...

try
{
// Execute some code that might throw an exception.
}
catch( CException* e )
{
// Handle the exception here.
// "e" contains information about the exception.
e->Delete();
}

// Other normal program statements

  瞧瞧,代码看上去显得那么整齐、干净,try block和catch block遥相呼应,多有对称美呀!因此主人公初次见面后就一见钟情了。

  为什么要选用异常处理的编程方法?
  当然更为重要的是,C++中引入的异常处理的编程机制提供给程序员一种全新的、更好的编程方法和思想。在C++中明确提出trycatch异常处理编程方法的框架之前的年代,程序员是怎样编写程序的,如下:

void main(int argc, char* argv[])
{
if (Call_Func1(in, param out)
{
// 函数调用成功,我们正常的处理
if (Call_Func2(in, param out)
{
// 函数调用成功,我们正常的处理
while(condition)
{
//do other job
if (has error)
{
// 函数调用失败,表明程序执行过程中出现一些错误,
// 因此必须处理错误
process_error();
exit();
}
//do other job
}
}
else
{
// 函数调用失败,表明程序执行过程中出现一些错误,
// 因此必须处理错误
process_error();
exit();
}

}
else
{
// 函数调用失败,同样是错误处理
process_error();
exit();
}
}

  因为程序的执行过程中总会遇到许多可预知或不可预知的错误事件,例如说,由于内存资源有限导致需要分配的内存失败了;或某个目录下本应存在的一个文件找不着了;或说不小心被零除了、内存越界了、数组越界了等等。这些错误事件存在非常大的隐患,因此程序员总需要在程序中不断加入if语句,来判断是否有异常出现,如果有,就必须要及时处理,否则可能带来意想不到的,甚至是灾难性的后果。这样一来,程序可读性差了很多,总是有许多与真正工作无关的代码,而且也给程序员增加了极大的工作负担,多数类似的处理错误的代码模块就像满山的牛屎一样遍地都是(程序员不大多是“牛”人吗?所以。。。哈哈)。

  但C++中的异常处理的机制彻底改变了这种面貌,它使真正的计算处理和错误处理分开来,让程序员不再被这些琐碎的事情所烦扰,能关注于真正的计算处理工作。同时代码的可读性也好了。因此我们有理由选择异常处理的编程方法。具体原因如下:
  1、 把错误处理和真正的工作分开来;
  2、 代码更易组织,更清晰,复杂的工作任务更容易实现;
  3、 毫无疑问,更安全了,不至于由于一些小的疏忽而使程序意外崩溃了;
  4、 由于C++中的try catch可以分层嵌套,所以它提供了一种方法使得程序的控制流可以安全的跳转到上层(或者上上层)的错误处理模块中去。(不同于return语句,异常处理的控制流是可以安全地跨越一个或多个函数 )。
  5、 还有一个重要的原因就是,由于目前需要开发的软件产品总是变得越来越复杂、越来越庞大,如果系统中没有一个可靠的异常处理模型,那必定是一件十分糟糕的局面。

  相信绝大多数程序员都知道C++中的异常处理的编程方法,可还是有很多人已习惯原来单纯的面向过程的代码组织方式,不太习惯或较少使用trycatch异常处理。为了使您编写的代码更安全;为了使您编写的代码让他人更易阅读,主人公阿愚强烈建议在您书写的代码中尽可能多用异常处理机制,少一些不必要的if判断语句。

  下一集详细介绍C++中的异常处理的语法。

clq
2007-9-10 14:52:31 发表 编辑



  如果您喜欢玩一款游戏,您必须先要很好理解这款游戏的规则。同样主人公阿愚喜欢上C++中异常处理后,当然也首先关注它的游戏规则,这就是C++中异常处理的语法。

关键字

  1、 try
  2、 catch
  3、 throw
  其中关键字try表示定义一个受到监控、受到保护的程序代码块;关键字catch与try遥相呼应,定义当try block(受监控的程序块)出现异常时,错误处理的程序模块,并且每个catch block都带一个参数(类似于函数定义时的数那样),这个参数的数据类型用于异常对象的数据类型进行匹配;而throw则是检测到一个异常错误发生后向外抛出一个异常事件,通知对应的catch程序块执行对应的错误处理。


语法

  1、还是给一个例子吧!如下:

int main()
{
cout << "In main." << endl;

//定义一个try block,它是用一对花括号{}所括起来的块作用域的代码块
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;

//这里抛出一个异常(其中异常对象的数据类型是int,值为1)
//由于在try block中的代码是受到监控保护的,所以抛出异常后,程序的
//控制流便转到随后的catch block中
throw 1;

cout << "在 try block 中, 由于前面抛出了一个异常,因此这里的代码是不会得以执行到的" << endl;
}
//这里必须相对应地,至少定义一个catch block,同样它也是用花括号括起来的
catch( int& value )
{
cout << "在 catch block 中, 处理异常错误。异常对象value的值为:"<< value << endl;
}

cout << "Back in main. Execution resumes here." << endl;
return 0;

}

  2、语法很简单吧!的确如此。另外一个try block可以有多个对应的catch block,可为什么要多个catch block呢?这是因为每个catch block匹配一种类型的异常错误对象的处理,多个catch block呢就可以针对不同的异常错误类型分别处理。毕竟异常错误也是分级别的呀!有致命的、有一般的、有警告的,甚至还有的只是事件通知。例子如下:

int main()
{
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;

cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}

return 0;
}

  3、一个函数中可以有多个trycatch结构块,例子如下:

int main()
{
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}

//这里是二个trycatch结构块,当然也可以有第三、第四个,甚至更多
try
{
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}

return 0;
}

  4、上面提到一个try block可以有多个对应的catch block,这样便于不同的异常错误分类处理,其实这只是异常错误分类处理的方法之一(暂且把它叫做横向展开的吧!)。另外还有一种就是纵向的,也即是分层的、trycatch块是可以嵌套的,当在低层的trycatch结构块中不能匹配到相同类型的catch block时,它就会到上层的trycatch块中去寻找匹配到正确的catch block异常处理模块。例程如下:

int main()
{
try
{
//这里是嵌套的trycatch结构块
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}

cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}

return 0;
}

  5、讲到是trycatch块是可以嵌套分层的,并且通过异常对象的数据类型来进行匹配,以找到正确的catch block异常错误处理代码。这里就不得不详细叙述一下通过异常对象的数据类型来进行匹配找到正确的catch block的过程。

  (1) 首先在抛出异常的trycatch块中查找catch block,按顺序先是与第一个catch block块匹配,如果抛出的异常对象的数据类型与catch block中传入的异常对象的临时变量(就是catch语句后面参数)的数据类型完全相同,或是它的子类型对象,则匹配成功,进入到catch block中执行;否则到二步;

   (2) 如果有二个或更多的catch block,则继续查找匹配第二个、第三个,乃至最后一个catch block,如匹配成功,则进入到对应的catch block中执行;否则到三步;

   (3) 返回到上一级的trycatch块中,按规则继续查找对应的catch block。如果找到,进入到对应的catch block中执行;否则到四步;

   (4) 再到上上级的trycatch块中,如此不断递归,直到匹配到顶级的trycatch块中的最后一个catch block,如果找到,进入到对应的catch block中执行;否则程序将会执行terminate()退出。

  另外分层嵌套的trycatch块是可以跨越函数作用域的,例程如下:

void Func() throw()
{
//这里实际上也是嵌套在里层的trycatch结构块
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
//由于这个trycatch块中不能找到匹配的catch block,所以
//它会继续查找到调用这个函数的上层函数的trycatch块。
throw 1;
}
catch( float& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
}

int main()
{
try
{
Func();

cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
catch( int& value )
{
//这个例子中,Func()函数中抛出的异常会在此被处理
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}

return 0;
}

  6、刚才提到,嵌套的trycatch块是可以跨越函数作用域的,其实这里面还有另外一层涵义,就是抛出异常对象的函数中并不一定必须存在trycatch块,它可以是调用这个函数的上层函数中存在trycatch块,这样这个函数的代码也同样是受保护、受监控的代码;当然即便是上层调用函数不存在trycatch块,也只是不能找到处理这类异常对象错误处理的catch block而已,例程如下:

void Func() throw()
{
//这里实际上也是嵌套在里层的trycatch结构块
//由于这个函数中是没有trycatch块的,所以它会查找到调用这个函数的上
//层函数的trycatch块中。
throw 1;
}

int main()
{
try
{
//调用函数,注意这个函数里面抛出一个异常对象
Func();

cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
catch( int& value )
{
//这个例子中,Func()函数中抛出的异常会在此被处理
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}

//如果这里调用这个函数,那么由于main()已经是调用栈的顶层函数,因此不能找
//到对应的catch block,所以程序会执行terminate()退出。
Func();
// [特别提示]:在C++标准中规定,可以在程序任何地方throw一个异常对象,
// 并不要求一定只能是在受到try block监控保护的作用域中才能抛出异常,但
// 如果在程序中出现了抛出的找不到对应catch block的异常对象时,C++标
// 准中规定要求系统必须执行terminate()来终止程序。
// 因此这个例程是可以编译通过的,但运行时却会异常终止。这往往给软件
// 系统带来了不安全性。与此形成对比的是java中提供的异常处理模型却是不
// 永许出现这样的找不到对应catch block的异常对象,它在编译时就给出错误
// 提示,所以java中提供的异常处理模型往往比C++要更完善,后面的章节
// 会进一步对这两种异常处理模型进行一个详细的分析比较。
return 0;
}

  朋友们!C++中的异常处理模型的语法很简单吧!就是那么(one、two、three、…哈哈!数数呢!)简单的几条规则。怪不得主人公阿愚这么快就喜欢上她了,而且还居然像一个思想家一样总结出一条感想:好的东西往往都是简单的,简单就是美吗!哈哈!还挺臭美的。

  下一集主人公阿愚愿和大家一起讨论一下C++中的异常处理中的一种特殊的catch用法,那就是关于catch(…)大探秘。

clq
2007-9-10 14:53:01 发表 编辑



  上一篇文章中详细讲了讲C++异常处理模型的trycatch使用语法,其中catch关键字是用来定义catch block的,它后面带一个参数,用来与异常对象的数据类型进行匹配。注意catch关键字只能定义一个参数,因此每个catch block只能是一种数据类型的异常对象的错误处理模块。如果要想使一个catch block能抓获多种数据类型的异常对象的话,怎么办?C++标准中定义了一种特殊的catch用法,那就是” catch(…)”。

感性认识

  1、catch(…)到底是一个什么样的东东,先来个感性认识吧!看例子先:

int main()
{
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;
//这里抛出一个异常(其中异常对象的数据类型是int,值为1)
throw 1;
}
//catch( int& value )
//注意这里catch语句
catch( …)
{
cout << "在 catch(…) block 中, 抛出的int类型的异常对象被处理" << endl;
}
}

  2、哈哈!int类型的异常被catch(…)抓获了,再来另一个例子:

int main()
{
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;
//这里抛出一个异常(其中异常对象的数据类型是double,值为0.5)
throw 0.5;
}
//catch( double& value )
//注意这里catch语句
catch( …)
{
cout << "在 catch(…) block 中, double类型的异常对象也被处理" << endl;
}
}

  3、同样,double类型的异常对象也被catch(…)块抓获了。是的,catch(..)能匹配成功所有的数据类型的异常对象,包括C++语言提供所有的原生数据类型的异常对象,如int、double,还有char*、int*这样的指针类型,另外还有数组类型的异常对象。同时也包括所有自定义的抽象数据类型。例程如下:

int main()
{
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;
//这里抛出一个异常(其中异常对象的数据类型是char*)
char* p=0;
throw p;
}
//catch( char* value )
//注意这里catch语句
catch( …)
{
cout << "在 catch(…) block 中, char*类型的异常对象也被处理" << endl;
}
}


int main()
{
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;
//这里抛出一个异常(其中异常对象的数据类型是int[])
int a[4];
throw a;
}
//catch( int value[] )
//注意这里catch语句
catch( …)
{
cout << "在 catch(…) block 中, int[]类型的异常对象也被处理" << endl;
}
}

  4、对于抽象数据类型的异常对象。catch(…)同样有效,例程如下:

class MyException
{
public:
protected:
int code;
};

int main()
{
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;
//这里抛出一个异常(其中异常对象的数据类型是MyException)
throw MyException();
}
//catch(MyException& value )
//注意这里catch语句
catch( …)
{
cout << "在catch(…) block中, MyException类型的异常对象被处理" << endl;
}
}
对catch(…)有点迷糊?
1、究竟对catch(…)有什么迷糊呢?还是看例子先吧!
void main()
{
int* p = 0;

try
{
// 注意:下面这条语句虽然不是throw语句,但它在执行时会导致系统
// 出现一个存储保护错误的异常(access violation exception)
*p = 13; // causes an access violation exception;
}
catch(...)
{
//catch(…)能抓获住上面的access violation exception异常吗?
cout << "在catch(…) block中" << endl;
}
}

  请问上面的程序运行时会出现什么结果吗?catch(…)能抓获住系统中出现的access violation exception异常吗?朋友们!和我们的主人公阿愚一样,自己动手去测试一把!
结果又如何呢?实际上它有两种不同的运行结果,在window2000系统下用VC来测试运行这个小程序时,发现程序能输出"在catch(…) block中"的语句在屏幕上,也即catch(…) 能成功抓获住系统中出现的access violation exception异常,很厉害吧!但如果这个同样的程序在linux下用gcc编译后运行时,程序将会出现崩溃,并在屏幕上输出”segment fault”的错误信息。

  主人公阿愚有点急了,也开始有点迷糊了,为什么?为什么?为什么同样一个程序在两种不同的系统上有不同的表现呢?其原因就是:对于这种由于硬件或操作系统出现的系统异常(例如说被零除、内存存储控制异常、页错误等等)时,window2000系统有一个叫做结构化异常处理(Structured Exception Handling,SEH)的机制,这个东东太厉害了,它能和VC中的C++异常处理模型很好的结合上(实际上VC实现的C++异常处理模型很大程度上建立在SEH机制之上的,或者说它是SEH的扩展,后面文章中会详细阐述并分析这个久富盛名的SEH,看看catch(…)是如何神奇接管住这种系统异常出现后的程序控制流的,不过这都是后话)。而在linux系统下,系统异常是由信号处理编程方法来控制的(信号处理编程,signal processing progamming。在介绍unix和linux下如何编程的书籍中,都会有对信号处理编程详细的介绍,当然执著的主人公阿愚肯定对它也不会放过,会深入到unix沿袭下来的信号处理编程内部的实现机制,并尝试完善改进它,使它也能够较好地和C++异常处理模型结合上)。

  那么C++标准中对于这种同一个程序有不同的运行结果有何解释呢?这里需要注意的是,window2000系统下catch(…)能捕获住系统异常,这完全是它自己的扩展。在C++标准中并没有要求到这一点,它只规定catch(…)必须能捕获程序中所有通过throw语句抛出的异常。因此上面的这个程序在linux系统下的运行结果也完全是符合C++标准的。虽然大家也必须承认window2000系统下对C++异常处理模型的这种扩展确实是一个很不错的完善,极大得提高了程序的安全性。

为什么要用catch(…)这个东东?

  程序员朋友们也许会说,这还有问吗?这篇文章的一开始不就讲到了吗?catch(…)能够捕获多种数据类型的异常对象,所以它提供给程序员一种对异常对象更好的控制手段,使开发的软件系统有很好的可靠性。因此一个比较有经验的程序员通常会这样组织编写它的代码模块,如下:

void Func()
{
try
{
// 这里的程序代码完成真正复杂的计算工作,这些代码在执行过程中
// 有可能抛出DataType1、DataType2和DataType3类型的异常对象。
}
catch(DataType1& d1)
{
}
catch(DataType2& d2)
{
}
catch(DataType3& d3)
{
}
// 注意上面try block中可能抛出的DataType1、DataType2和DataType3三
// 种类型的异常对象在前面都已经有对应的catch block来处理。但为什么
// 还要在最后再定义一个catch(…) block呢?这就是为了有更好的安全性和
// 可靠性,避免上面的try block抛出了其它未考虑到的异常对象时导致的程
// 序出现意外崩溃的严重后果,而且这在用VC开发的系统上更特别有效,因
// 为catch(…)能捕获系统出现的异常,而系统异常往往令程序员头痛了,现
// 在系统一般都比较复杂,而且由很多人共同开发,一不小心就会导致一个
// 指针变量指向了其它非法区域,结果意外灾难不幸发生了。catch(…)为这种
// 潜在的隐患提供了一种有效的补救措施。
catch(…)
{
}
}

  还有,特别是VC程序员为了使开发的系统有更好的可靠性,往往在应用程序的入口函数中(如MFC框架的开发环境下CXXXApp::InitInstance())和工作线程的入口函数中加上一个顶层的trycatch块,并且使用catch(…)来捕获一切所有的异常,如下:

BOOL CXXXApp::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}

AfxEnableControlContainer();

// Standard initialization
// If you are not using these features and wish to reduce the size
// of your final executable, you should remove from the following
// the specific initialization routines you do not need.

#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif


// 注意这里有一个顶层的trycatch块,并且使用catch(…)来捕获一切所有的异常
try
{
CXXXDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
}
catch(…)
{
// dump出系统的一些重要信息,并通知管理员查找出现意外异常的原因。
// 同时想办法恢复系统,例如说重新启动应用程序等
}

// Since the dialog has been closed, return FALSE so that we exit the
// application, rather than start the application's message pump.
return FALSE;
}

  通过上面的例程和分析可以得出,由于catch(…)能够捕获所有数据类型的异常对象,所以在恰当的地方使用catch(…)确实可以使软件系统有着更好的可靠性。这确实是大家使用catch(…)这个东东最好的理由。但不要误会的是,在C++异常处理模型中,不只有catch(…)方法能够捕获几乎所有类型的异常对象(也许有其它更好的方法,在下一篇文章中主人公阿愚带大家一同去探讨一下),可C++标准中为什么会想到定义这样一个catch(…)呢?有过java或C#编程开发经验的程序员会发现,在它们的异常处理模型中,并没有这样类似的一种语法,可这里不得不再次强调的是,java中的异常处理模型是C++中的异常处理模型的完善改进版,可它反而没有了catch(…),为何呢?还是先去看看下一章吧,“C++的异常处理和面向对象的紧密关系”。也许大家能找到一个似乎合理的原因。

clq
2007-9-10 14:55:36 发表 编辑


如果有人问起C++和C到底有那些本质上的不同点?主人公阿愚当然也会有自己的一份理解,他会毫不犹豫回答出:“与C相比,C++至少引入了两项重要技术,其一就是对面向对象的全面支持;还有一项就是C++优良的异常处理模型”。是的,这两项技术对构建出一个优良的可靠复杂的软件系统都太重要了。可这两项技术之间又有何关系呢?非常客观公正的说,它们之间的关系实在是太紧密了,两者相互支持和依赖,是构建优良可靠复杂的软件系统最不可缺乏的两个东东。

用对象来描述程序中出现的异常

  虽然前几篇文章的内容中列举的一些小例子程序大多都是throw一些如int、double类型的异常,但程序员朋友都很熟悉,实际开发环境中所抛出的异常都是一个个代表抽象数据类型的对象,如C++标准库中的std::exception(),MFC开发库中Cexception等。用对象来描述的我们程序中的出现异常的类型和异常信息是C++异常处理模型中最闪光之处,而且这一特点一直沿用到java语言的异常处理模型中。

  为什么要用对象来描述程序中出现的异常呢?这样做的优势何在?主人公阿愚不喜欢穷摆出什么大道理,还是老办法,从具体的实例入手。由于异常有许许多多种类型,如有致命的错误、一般的错误、警告或其它事件通知等,而且不同类型的异常有不同的处理方法,有的异常是不可恢复的,而有的异常是可以恢复的(专业术语叫做“重入”吧!哈哈,主人公阿愚有时也会来点文绉绉的东西),所以程序员在开发系统时就必须考虑把各种可能出现的异常进行分类,以便能够分别处理。下面为一个应用系统设计出一个对异常进行分类的简单例子,如下:

clq
2007-9-10 15:00:18 发表 编辑

[图片]
  从上面的异常分类来看,它有明显的层次性和继承性,这恰恰和面向对象的继承思想如出一辙,因此用对象来描述程序中出现的异常是再恰当不过的了。而且可以利用面向对象的特性很好的对异常进行分类处理,例如有这样一个例子:

void OpenFile(string f)
{
try
{
// 打开文件的操作,可能抛出FileOpenException
}
catch(FileOpenException& fe)
{
// 处理这个异常,如果这个异常可以很好的得以恢复,那么处理完毕后函数
// 正常返回;否则必须重新抛出这个异常,以供上层的调用函数来能再次处
// 理这个异常对象
int result = ReOpenFile(f);
if (result == false) throw;
}
}

void ReadFile(File f)
{
try
{
// 从文件中读数据,可能抛出FileReadException
}
catch(FileReadException& fe)
{
// 处理这个异常,如果这个异常可以很好的得以恢复,那么处理完毕后函数
// 正常返回;否则必须重新抛出这个异常,以供上层的调用函数来能再次处
// 理这个异常对象
int result = ReReadFile(f);
if (result == false) throw;
}
}

void WriteFile(File f)
{
try
{
// 往文件中写数据,可能抛出FileWriteException
}
catch(FileWriteException& fe)
{
// 处理这个异常,如果这个异常可以很好的得以恢复,那么处理完毕后函数
// 正常返回;否则必须重新抛出这个异常,以供上层的调用函数来能再次处
// 理这个异常对象
int result = ReWriteFile(f);
if (result == false) throw;
}
}

void Func()
{
try
{
// 对文件进行操作,可能出现FileWriteException、FileWriteException
// 和FileWriteException异常
OpenFile(…);

ReadFile(…);

WriteFile(…);
}
// 注意:FileException是FileOpenException、FileReadException和FileWriteException
// 的基类,因此这里定义的catch(FileException& fe)能捕获所有与文件操作失败的异
// 常。
catch(FileException& fe)
{
ExceptionInfo* ef = fe.GetExceptionInfo();
cout << “操作文件时出现了不可恢复的错误,原因是:”<< fe << endl;
}
}

  通过上面简单的例子可以看出,利用面向对象的方法,确实能很好地对异常进行分类处理,分层处理,如果异常能得以恢复的尽可能去实现恢复,否则向上层重新抛出异常表明当前的函数不能对这里异常进行有效恢复。同时特别值得一提的是,上层的catch block利用申明一个基类的异常对象作为catch关键字的参数,使得提供了对多种类型的异常对象的集中处理方法,这就是上一篇文章中所提到的除了catch(…)以外,还有其它的来实现对多种类型的异常对象的集中处理方法,而且利用对象基类的方法显然要比catch(…)优雅很多,方便很多,要知道在catch(…)的异常处理模块中是没有多少办法获取一些关于异常出现时异常具体信息的,而对象基类的方法则完全不同,异常处理模块可以访问到真正的异常对象。

  现在回想一下上一篇文章中提出的那个问题?就是既然有其它很好的方法(利用类的继承性)来可以代替catch(…)提供的异常集中处理,那为什么C++标准中还偏要提供catch(…)这样一种奇怪的语法呢?其实这还是由于C++本身一些特点所决定的,因为大家都知道,C++在业界有很多的版本,更重要的是没有一个统一的标准开发类库,或者说没有统一的标准开发环境,虽然存在标准C库和标准C++库,但这远远不够,构成不了一个完整的开发支撑环境,因此在许多重要的开发库中都各自为政,它们在自己的开发库都各自定义了一套对异常进行分类支持的库。因此应用程序的开发环境往往都同时需要依赖于几个基础开发库之上(例如MFC + XML4C + Standard C++),这样对开发人员而言,便没有一个共同的异常对象的基类,所以C++标准中便提供了catch(…)来捕获所有异常,这确实是一种不得已而为之的折衷方法(哈哈!这个理解完全是主人公阿愚自己一相情愿,一个人胡思乱想而出来的,朋友们如有不同的意见可以和阿愚一起讨论!),另外JAVA中则不会出现这种情况,因为JDK是统一的,所有的异常对象都是从java.lang.Throwable接口继承而来的,因此只要在程序的入口函数中catch(java.lang.Throwable all),便可以捕获所有的异常。所以在JAVA的异常处理模型中没有类似C++那样一个catch(…)的东东,完全没必要。

异常处理中采用面向对象技术还有哪些好处呢?

  上面讲到,用对象来描述程序中出现的异常除了能很好地分层处理异常外,还有那些好处呢?当然除了好处大大的外,好处也是多多的,例如:

  (1) 面向对象的实现中,一般都很好的实现了对象的RTTI技术,如果异常用对象来表示,那么就可以很好完成异常对象的数据类型匹配,还有就是函数的多态,例如上面的那个例子中,即便是到了catch(FileException& fe)异常处理模块中,也能知道到底是出现了那种具体的异常,是FileOpenException呢?还是其它的异常?

  (2) 面向对象的实现中,一般都很好的实现了对象的构造、对象的销毁、对象的转存复制等等,所以这也为异常处理模型中,异常对象的转存复制和对象销毁提供了很好的支持,容易控制;
  (3) 其它的吗?暂时没有,可能还有没想到的。

  在异常处理的分层管理下,异常对象的重新抛出往往非常常见,本文中刚才的例子就有这样的情况,但当时仅一笔带过而已,为了表明对它的重视,下一篇文章重点讨论一下异常对象的rethrow处理。


引用地

clq
2007-9-10 15:00:48 发表 编辑




  上一篇文章已经提到了C++的异常rethrow现象,这篇文章将进一步深入讨论这个问题。当catch到一个异常后进入异常处理程序块中,此时需要从传入的异常对象中得到一些关于异常的详细信息,并判断这个异常是否能得以恢复,是否能在当前的异常处理程序块中得以处理。如果是,那么及时地处理掉这个异常,使程序恢复到正常的工作轨道上(也即从catch block后面的代码处继续执行);否则就必须重新抛出异常(Excption Rethrow),把这个异常交给上一层函数的异常处理模块去处理(反正我是处理不了了,而且我也通知了我的上层领导,所以责任吗,也就不由我担当了,哈哈 ^-^)。

语法

  很简单,有两种用法,如下:
  1、 throw ;
  2、 throw exception_obj ;
  第一种表示原来的异常对象再次被重新抛出;第二中呢,则表示原来的异常已处理或正在处理中,但此时又引发了另一个异常。示例如下:

void main()
{
try
{
try
{
throw 4;
}
catch(int value)
{
// 第一种用法,原来的异常被再次抛出
// 注意它不需要带参数。
throw;
}

try
{
throw 0.5;
}
catch(double value)
{
// 第二种用法,再次抛出另外的一个异常
// 它的语法和其它正常抛出异常的语法一样。
throw “another exception”;
}
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

在什么地方异常可以rethrow?

  当然,异常的rethrow只能在catch block中,或者说在catch block中抛出的异常才是异常的rethrow,因此注意下面的示例程序中存在语法错误,如下:

void main()
{
try
{
try
{
throw 4;
}
catch(int value)
{
// 这里的语法是对的。
throw;
}

// 但这里的语法却是不对的。
// 不能在这里进行异常的rethrow
throw;
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

异常rethrow需要注意的问题!
异常rethrow需要注意什么问题呢?看例子先!

void main()
{
try
{
try
{
throw 4;
}
catch(int value)
{
// 异常的rethrow
throw;
}
catch(...)
{
cout << “能打印我这条消息吗?”<< endl;
}
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

  上面的程序运行结果是:“unknow exception”
  由此我们可以得出结论,异常的rethrow后,它会在它上一层的trycatch块开始查找匹配的catch block异常处理块,而在同一层中,如果当前的catch block后面还有其它的catch block,它是不会去匹配的。所以程序中一般层次模型的trycatch要比线性结构的trycatch要好一些,如下(示例2要比示例1好):

// 示例1
void main()
{
try
{
}
catch(DataType1&)
{
}
catch(DataType2&)
{
}
catch(DataType3&)
{
}
catch(...)
{
}
}

// 示例2
void main()
{
try
{
try
{
try
{
try
{
}
catch(DataType1&)
{
}
}
catch(DataType2&)
{
}
}
catch(DataType3&)
{
}
}
catch(...)
{
}
}

总结

  相遇篇的文章到此结束。通过这几篇文章的介绍,目前已经对异常处理编程的思想,C++异常处理模型、语法,以及C++异常处理与面向对象的关系等等,都有了一个大概性的了解。主人公阿愚根据自己的理解和经验,现在对相遇篇中的知识再做出如下一些总结:
  (1) 异常处理编程和面向对象,是现在程序设计和编程中最不可缺少的两个好东东;
  (2) C++异常处理模型是层次型的,能很好地支持嵌套;
  (3) C++异常处理编程提供try、catch和throw三个关键字。其中try定义受监控的程序块;catch定义异常处理模块;throw让程序员可以在程序执行出错的地方抛出异常;
  (4) C++异常处理模型的实现充分使用到了面向对象的思想和方法;
  (5) C++异常处理模型中,异常是可以rethrow的。

  从下篇文章开始,主人公阿愚对异常处理编程将进入到了一个相知的阶段。这一阶段中,阿愚将全面性地去深入了解异常处理编程中的各个细节和一些特点,并根据自己的理解阐述一些异常处理编程设计思想方面的东西。各位程序员朋友们,准备好了吗?Let's go!

clq
2007-9-10 15:01:14 发表 编辑




  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!

clq
2007-9-10 15:01:55 发表 编辑




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

构造函数中抛出的异常

  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++的异常处理不会破坏任何一条面向对象的特性!”,因此主人公阿愚再次建议朋友们,牢牢记住这一条!

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

clq
2007-9-10 15:02:15 发表 编辑





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

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

  对于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!

clq
2007-9-10 15:02:29 发表 编辑



  在相遇篇的第4集文章中,曾经讲到过在C++的异常处理模型中,是用“对象”来描述程序中出现的异常,并且在那篇文章中详细讨论了这样做所带来的诸多好处,其中之一呢就是:对象一般都很好地实现了对象的构造、对象的销毁、对象的转存复制,这为异常处理模型中异常对象的转存复制和对象销毁提供了很好的支持。是的没错,但是所谓的异常对象到底是如何被复制和传递呢?从本篇文章开始,和接下来的几篇文章中,主人公阿愚将和大家一同比较深入地探讨这个问题,并力求弄清每一个重要的细节。

概述

  呵呵!sorry,居然忘了阐述一下定义。那就是“C++的异常对象被传递”指的是什么?想当然大家也都知道,这指的就是异常出现时throw出的异常对象如何被传递到catch block块中,catch block中的异常处理模块再根据异常对象提供的异常信息做出相应的处理。程序员朋友们也许认为这很简单,其实说简单也好像不太简单,因为这种对象的传递或复制可能发生在同一个函数的不同程序块作用域间,也有可能是从当前的函数传递到上一个函数中,更有可能是把异常对象传递复制到上上(甚至更多层)的函数中。

  异常对象的传递有点类似于函数调用过程中的参数传递的过程。瞧!catch关键字的语法不就跟函数的定义有点类似吗?作为入参的异常对象也是用括号被括起来的,只不过catch只能是拥有一个参数。另外连catch(…)语法也是抄袭函数定义的方式,表示接受任意类型的数据对象。

  C++程序中函数的调用是通过“栈”来实现的,其中参数的传递也是保存到栈中,以实现两个函数间的数据共享。那么异常对象的传递呢?当然也是通过栈,其实这是很明显的一件事情,因为异常对象本身肯定是局部变量,因此它也肯定是被保存在栈中的。不过异常对象的传递毕竟还是与函数参数的传递有很大的不同,函数参数的传递是严谨的、一级一级的对象数据的压栈过程和出栈过程;但异常对象的传递却远比这要复杂些,因为它这是逆序的,属于局部变量的异常对象可能要往上层(或更上层)函数传递,它的过程是一个跳跃式的或比较混乱的过程。关于异常对象的传递具体是如何实现的,在爱的秘密篇中分析C++异常处理模型的实现时会再做详细阐述。而目前需要搞清楚的是,这个过程中所需要遵从的一些规律或标准。

  函数的参数的传递一般有指针、传值和引用三种方式,同样,异常对象的传递也同样有这三种方式的区别。现在开始,主人公阿愚分别讲述每一种方式下异常对象是如何被传递的,不过在正式开始之前,还是先简要总结函数调用的过程,以及这过程栈的变化。因为这对随后的具体分析和理解也许大有帮助。

函数的调用过程与“栈”

  C++程序员对这个过程肯定非常熟悉,因此这里不做细致的讲述,只做一个概要性的总结。
  (1) 函数的调用过程实质上利用栈来实现的指令(eip)执行远程转移和返回的过程;它在CPU指令级别上就得到了支持(CALL和RET指令);
  (2) 每个线程都有一个自己的栈,因此每个线程的函数调用执行是相互不受影响的;
  (3) C和C++中的函数参数的入栈顺序一般是从右到左进行;
  (4) C++中的函数的参数的传递一般有指针、传值和引用三种方式;
  (5) C和C++中函数的返回值一般都是保存到EAX寄存器中返回的;
  (6) C和C++中函数中寻址参数和局部变量,一般都是通过EBP寄存器加上偏移来进行的,如参数一般是:[EBP+XX],而局部变量则一般是:[EBP-XX];
  (7) 在程序运行时,EBP中的值一般是指向当前的函数调用帧,而ESP一般指向栈顶。

  如果对上面论述有不太清楚或不太熟悉的朋友,建议先看看专门讲述C++设计和编程方面的书籍。下面给出一个线程运行期间,它栈中所保存的数据的布局(部分片段),如下图:


总数:61 页次:1/7 首页 下一页  >>  尾页  
总数:61 页次:1/7 首页 下一页  >>  尾页  


所在合集/目录



发表评论:
文本/html模式切换 插入图片 文本/html模式切换


附件:



NEWBT官方QQ群1: 276678893
可求档连环画,漫画;询问文本处理大师等软件使用技巧;求档softhub软件下载及使用技巧.
但不可"开车",严禁国家敏感话题,不可求档涉及版权的文档软件.
验证问题说明申请入群原因即可.

Copyright © 2005-2020 clq, All Rights Reserved
版权所有
桂ICP备15002303号-1