| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 1039 人关注过本帖
标题:[转帖]异常处理
只看楼主 加入收藏
live41
Rank: 10Rank: 10Rank: 10
等 级:贵宾
威 望:67
帖 子:12442
专家分:0
注 册:2004-7-22
结帖率:66.67%
收藏
 问题点数:0 回复次数:0 
[转帖]异常处理

异常处理 作者:Danny Kalev 翻译:Kyle

简介 大型应用软件往往是分层构建的。在最底层你会发现库函数,API函数,和私有的底层函数。然而在最高层则是用户接口组件,比如一个电子制表软件让用户填写数据表单。下面再来考虑一种普通的航空订票系统:它的最高端是由一些GUI组件所组成,用来在用户的屏幕上显示内容。这些高端组件与那些封装了数据库API的数据存取对象相结合。再底层一些,那些数据库API与数据库引擎相交互,然而数据库引擎自己又会调用系统服务来处理底层的硬件资源,比如物理内存,文件系统和安全模型。一般情况下,及其严格的运行期错误会在这些底层代码中被检测出来,但是它们不能-----或者说不应该----试图自己处理这些错误。解决这些严格的运行期错误的责任应该由高端组件来承担。为了解决一个错误,高端组件必须得到错误发生的通知。本质上,错误处理包括错误检测和通知高端组件。这些组件依次处理错误并且试图从错误中恢复。

传统的错误处理方法 在早些时期,C++本身并没有能力来处理运行期错误。取而代之的是那些传统的C方法。这些方法可以被归为三类设计策略: 1. 返回一个状态码来表明成功或失败 2. 把错误码赋值给一个全局变量并且让其他的函数来检测 3. 终止整个程序 上述的任何一个方法在面向对象环境下都有明显的缺点和限制。其中的一些根本就不可接受,尤其是在大型应用程序中。接下来的部分将会仔细检查一下这些方法,目的是发现他们与生俱来的限制和危险性。

返回一个错误码 在某种程度上这个方法是有用的,比如一个小型程序有着一致而且有限的错误码存在,并且严格的报告错误和检查一个函数返回值的策略被应用。然而,这种方法有着显著的局限性;例如,错误类型和它们的列举值必须标准化。因为一个库的实现者可能选择返回值0来代表一个错误,然而另一个实现者却选择0来代表成功并且用那些非0值代表出现错误。通常,那些返回码会在一个公共头文件中以符号常量的形式存在,从而在整个软件的开发过程中或者在一个开发团队里达成一致。但是,这些码并不是标准的。 不用说,在结合那些不兼容的软件库的时候,面对非标准的错误码将会变得非常困难。另外一个缺点是对于每一个返回码都必须查阅和解释------一个乏味并且昂贵的操作。这个策略需要每一个调用者在每一次调用的时候对返回值进行检查,如果没有这样做将会导致运行期错误。当一个错误码被检测,就会终止正常的执行流程并且把错误码传递给调用者。那些附加的包裹每一个函数调用的代码会很轻易的使程序的大小翻倍并且引起软件维护和程序可读性的困难。更糟的是,有时要想返回一个error value是不可能的。例如,构造函数没有返回值,所以就不能应用这种方法在对象构造失败的情况下报告错误。

求助于全局标记 一个可以选择的用来报告运行期错误的途径是使用全局标记,它表明了最后的操作是否成功。不像返回码策略,这个方法是标准化的。C 的<errno.h>头文件中定义了一种机制用来检查和给一个全局整型标记errno赋值。这种策略固有的缺陷是不能被忽视的。在一个多线程环境中,被一个线程赋予了一个错误码的errno有可能不经意的被另一个线程所改写,而调用者还未对errno进行检查。另外,对错误码而不是一个更为可读的信息的使用是很不利的,因为那些错误码可能会在不同的环境中不兼容。最终,这种方法需要严格的良好的编程样式,那就是不断的对errno的当前值进行检查。 全局标记策略和函数返回值策略是相似的:二者都提供一种机制来报告错误,但是二者却都不能保证错误被处理。例如,一个函数没有成功打开一个文件可以通过给errno赋予一个合适的值来表明错误的发生。然而,它不能阻止另一个函数试图写入和关闭那个文件。更进一步,如果errno表明一个错误并且程序员检测到而且按照预期处理了它,那么errno还应该被显式的复位。如果一个程序员忘记了做这件事,那么将会引起其他函数误以为错误还没有被处理,从而去校正那个问题,从而引起不可预知的结果。

终止程序 最为残酷的处理运行期错误的方法是简单的终止程序。这种解决方案去除了上面两种方法的一些缺点;例如,没有必要反复的检查每个函数返回值的状态,而且程序员也不必赋值给一个全局标记,反复的测试和清除它的值。在标准C的函数库中有两个函数用来终止一个程序:exit()和abort()。exit()被调用能够表明程序被成功终止,或者它可以在遇到运行期错误的时候被调用。在把控制权交还给运行环境之前,exit()首先会清空流和关闭打开的文件。abort()却不一样,它表示程序被意外终止,不会清空流和关闭打开的文件。 关键性的程序不应该在任何运行期错误存在的情况下突然终止。如果一个生命支持系统突然停止工作仅仅是因为它的控制器检测到0做除数,那么将是可怕的。同样,一个控制由人驾驶的航天飞机自动运行的计算机系统也不应该因为暂时的和地面控制系统失去联系就停止工作。类似的,电话公司的账目系统或者银行系统都不应该在运行期错误出现的时候就中止。健壮的真实世界的应用程序应该做的更好。 程序终止甚至对于应用程序都是有问题的。一个检测到错误的函数通常都没有必要的信息来衡量错误的严重性。例如一个内存分配函数并不能说出内存分配失败是由于用户正在使用调试器,网页浏览器,电子制表软件,文字处理软件,还是由于系统因为硬件错误变得不稳定。在第一种情况下,系统可以简单的显示一条信息来告诉用户关闭不必要的应用程序。第二种情况下,就需要一种更为残酷的措施了。然而,在终止程序的策略下,那个内存分配函数就会简单的终止程序,而不考虑错误的严重性。这种方法在一些关键性应用程序中是无法应用的。好的系统设计应该保证运行期错误被检测和报告,但是它也应该确保最小限度的容错水平。 终止程序在极限环境下或者在调试阶段是可以被接受的。然而,abort()和exit()却不应该在面向对象环境中使用,甚至即使在调试阶段,因为他们并没有意识到C++对象模型的存在。

exit()和abort()不销毁对象 对象可以持有从构造函数或者某个成员函数中获得的资源:从free store中分配的内存,文件句柄,通信端口,I/O设备等等。这些资源必须在适当时候被释放。通常,资源都是由析构函数来释放。这种设计方法被称为resource initialization is acquisition。在栈上建立的局部对象会自动销毁。然而abort() 和exit()都不调用这些局部对象的析构函数。因此,程序的意外终止将会引起无法挽回的损害:数据库被破坏,文件可能丢失,并且一些有价值的数据可能丢失。基于这个原因,请不要在面向对象环境中使用abort()和exit()。

进入异常处理 正如你所见,传统C的错误处理方法并不适合C++,C++的一个设计目标就是让用C++进行大规模软件开发比C更好更安全。 C++的设计者已经意识到缺乏合适的错误处理机制使得实现这一目标相当的困难。他们试图寻找一种完全摆脱传统C的错误处理缺陷的解决方案。其中的一种想法就是建立在当异常被触发的时候自动把控制权传递给系统。机制必须简单,并且它能够使程序员从不断的检查一个全局标志或者返回值的苦差事中解脱出来。另外,它还必须保证异常处理程序能够自动的获得异常信息。最终它还要保证当一个异常没有在本地处理的时候,本地对象能够被适当的销毁,并且把它所持有的资源释放。 1989年,在多年的研究和多方建议下,异常处理进入C++。C++并不是第一个提供结构化运行期错误处理支持的语言。早在20世纪60年代,PL/1就提供了一种内建的异常处理机制;Ada也在20世纪80年代提供了自己的异常处理,另外还有几种语言也做到了这一点。但是这些异常处理模型没有一个适合C++对象模型和程序结构。因此,被提议的C++异常处理是独一无二的,并且它已经作为了一种模型出现在一些新产生的语言之中。 异常处理机制的实现被证明是一种挑战。第一个C++编译器,cfront,在UNIX环境下运行。和许多UNIX编译器一样,它是一个翻译器首先把C++代码转换成C,接着再编译C代码。Cfront 4.0计划引入异常处理,然而,异常处理机制的实现所遇到需要变得如此复杂,以至于cfront 4.0的开发团队在用了一年时间设计它之后完全的放弃了这个项目。Cfront 4.0再也没有出台。然而,异常处理成为了标准C++的有机组成部分。后来出现的一些编译器都支持了它。在接下来的部分里将会解释为什么在cfront以及任何编译器下实现异常处理是如此的困难。

实现异常处理所面临的挑战 实现异常处理所遇到的困难主要来自于以下几个因素:第一,实现必须保证对于特定异常的合适的handler被找到。 第二,异常对象必须是多态的;这样,当实现无法通过派生类对象定位handler的时候可以考虑基类的handler。这种需要表明必须引入运行期类型检测。然而那时C++还没有任何运行期类型检测的能力。因此这种能力必须首先被实现。 作为一个附加的复杂性,实现必须能够调用所有局部对象的析构函数。这个过程被称为stack unwinding 。因为早期的C++编译器首先要把C++源文件转换为纯C,然后再把代码编译成机器码。异常处理的实现者们不得不用C来实现运行期类型鉴别和stack unwinding。幸运的是,这些障碍已经被克服。

应用异常处理 异常处理是一种灵活并且精巧的工具。它克服了C的传统错误处理方法的缺点并且它能够被用来解决一系列运行期错误。但是,异常处理像其他语言特性一样,很容易被误用。为了能够有效的使用这一特性,理解运行期机器是如何工作的以及相关的性能花费是非常重要的。接下来的部分里将会进入异常处理的内部并且论证如何使用这一工具来建立安全的应用系统。

异常处理要素 异常处理是一种把控制权从异常发生的地方转移到一个匹配的handler的机制。异常是内建数据类型变量或者是对象。异常处理机制包括四个部分:a try block,一个或多个和try block相关的handler,throw表达式,以及异常自己。Try block包含可能抛出异常的代码。例如: try { int * p = new int[1000000]; //may throw std::bad_alloc } 一个try block后面将跟有一个或多个catch语句或者说是handlers,这些handlers的每一个处理不同类型的异常。例如: try { int * p = new int[1000000]; //may throw std::bad_alloc //... } catch(std::bad_alloc& ) { } catch (std::bad_cast&) { } handler仅仅被在try block中的throw表达式以及函数所调用。Throw表达式包括一个关键字throw以及assignment expression。例如: try { throw 5; // 5 is assigned to n in the following catch statement } catch(int n) { } throw表达式和返回语句很相似。Empty throw是没有操作数的throw语句。例如:

throw;

在handler中的empty throw表明它是一个rethrow,后面我们会讨论到它。另外,如果目前没有异常被处理,那么执行一个empty throw将会调用terminate()。

搜索更多相关主题的帖子: 数据库 订票 应用软件 物理 内存 
2004-12-04 15:24
快速回复:[转帖]异常处理
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.022178 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved