《C++编程思想17》由会员分享,可在线阅读,更多相关《C++编程思想17(23页珍藏版)》请在金锄头文库上搜索。
1、下载下载第1 7章异 常 处 理错误修复技术的改进是提高代码健壮性的最有效方法之一。但是,大多数程序设计人员在实际设计中往往忽略出错处理 ,似乎是在没有错误的状态下编程。毫无疑问,出错处理的繁琐及错误检查引起的代码膨胀是导致上述问题的主要原因。例如,虽然printf( )函数可返回打印参数的个数,但是实际程序设计中没有人检查该值。出错处理引起的代码膨胀将不可避免地增加程序阅读的困难,这对于程序设计人员来说是十分令人烦恼的。C语言中实现出错处理的方法是将用户函数与出错处理程序紧密地结合起来,但是这将造成出错处理使用的不方便和难以接受。异常处理是C + +语言的一个主要特征,它提出了出错处理更加完
2、美的方法。1) 出错处理程序的编写不再繁琐,也不须将出错处理程序与“通常”代码紧密结合。在错误有可能出现处写一些代码,并在后面的单独节中加入出错处理程序。如果程序中多次调用一个函数,在程序中加入一个函数出错处理程序即可。2) 错误发生是不会被忽略的。如果被调用函数需发送一条出错信息给调用函数,它可向调用函数发送一描述出错信息的对象。如果调用函数没有捕捉和处理该错误信号,在后续时刻该调用函数将继续发送描述该出错信息的对象,直到该出错信息被捕捉和处理。在这一章中我们将讨论C语言的出错处理方法,讨论为何该方法在 C语言中不是很理想的,并且无法在C + +中使用;然后学习t r y,t h r o w
3、和c a t c h的用法,它们在C + +中支持异常处理。17.1 C语言的出错处理本书在第8章以前使用C标准库的assert( )宏作为出错处理的方法。第 8章以后assert( )被按照原先的设计目的使用:在开发过程中,使用它们,完成后用 #define NDEBUG使之失效,以便推出产品。为了在运行时检查错误, assert( )被allege( )函数和第8章中引入的宏所取代。通常我们会说: “对于出错处理我们必须面对复杂的代码,但是在这个例子中我们不必由此感到烦恼” 。allege( )函数对一些小型程序很方便,对于复杂的大型程序,所编写的出错处理程序也将更加复杂。在通过检查条件我
4、们能确切地知道做什么的情况下,出错处理就变得十分明确和容易了,因为我们通过上下文得到了所有必要的信息。当然,我们只是在这一点上处理错误。这些都是十分普通的错误,不是这一章的主题。若错误问题发生时在一定的上下文环境中得不到足够的信息,则需要从更大的上下文环境中提取出错处理信息,下面给出了C语言处理这类情况的三种典型方法。1) 出错信息可通过函数的返回值获得。如果函数返回值不能用,则可设置一全局错误判断标志(标准C语言中errno( )和perror( )函数支持这一方法) 。正如前文提到的,由于对每个函数调用都进行错误检查,这十分繁琐并增加了程序的混乱度。程序设计者可能简单地忽略这些出错信息,因
5、为乏味而迷乱的错误检查必须随着每个函数调用而出现。另外,来自偶然出现异常的函数的返回值可能并不反映什么问题。2) 可使用C标准库中一般不太熟悉的信号处理系统,利用 s i g n a l ( )函数(判断事件发生的类型)和r a i s e ( )函数(产生事件) 。由于信号产生库的使用者必须理解和安装合适的信号处理系统,所以应紧密结合各信号产生库,但对于大型项目,不同库之间的信号可能会产生冲突。3) 使用C标准库中非局部的跳转函数:setjmp( ) 和 longjmp( )。setjmp( ) 函数可在程序中存储一典型的正常状态,如果进入错误状态, longjmp( )可恢复setjmp(
6、 ) 函数的设定状态,并且状态被恢复时的存储地点与错误的发生地点紧密联系。考虑C + +语言的出错处理方案时会存在另一个关键性问题:由于 C语言的信号处理技术和s e t j m p / l o n g j m p函数不能调用析构函数,所以对象不能被正确地清除。由于对象不能被清除,它将被保留下来并且将不能再次被存取,所以存在这种问题时实际上是不可能有效正确地从异常情况中恢复出来。下面的例子将演示s e t j m p / l o n g j m p的这一特点:s e t j m p ( )是一个特别的函数,因为如果我们直接调用它,它就把当前进程状态的所有相关信息存放在 j m p _ b u
7、f中,并返回零。这样,它的行为象通常的函数。然而,如果使用同一个j m p _ b u f调用l o n g j m p ( ),这就象再次从s e t j m p ( )返回,即正确地弹出s e t j m p ( )的后端。这时,返回值对于l o n g j m p ( )是第二个参数,所以能发现实际上从l o n g j m p ( )中返回了。可以想象,有多个第17章 异 常 处 理361下载不同的j m p _ b u f,可以弹出程序的多个不同位置的信息。局部 g o t o(用标号)和这个非局部跳转的不同在于我们能通过s e t j m p / l o n g j m p跳转到任
8、何地方(一些限制不在这里讨论) 。在C+中的问题是,longjmp()不适用于对象,特别是,当它跳出范围时它不调用析构函数1。析构函数调用是必须的,所以这种方法在C+中不可行。17.2 抛出异常如果程序发生异常情况,而在当前的上下文环境中获取不到异常处理的足够信息,我们可以创建一包含出错信息的对象并将该对象抛出当前上下文环境,将错误信息发送到更大的上下文环境中。这称为异常抛出。如:throw myerror (something bad happened);m y e r r o r是一个普通类,它以字符变量作为其参数。当进行异常抛出时我们可使用任意类型变量作为其参数(包括内部类型变量) ,但
9、更为常用的办法是创建一个新类用于异常抛出。关键字t h r o w的引入引起了一系列重要的相关事件发生。首先是 t h r o w调用构造函数创建一个原执行程序中并不存在的对象。其次,实际上这个对象正是 t h r o w函数的返回值,即使这个对象的类型不是函数设计的正常返回类型。对于交替返回机制,如果类推太多有可能会陷入困境,但仍可看作是异常处理的一种简单方法,可通过抛出一个异常来退出普通作用域并返回一个值。因为异常抛出同常规函数调用的返回地点完全不同,所以返回值同普通函数调用具有很小的相似性(异常处理器地点与异常抛出地点可能相差很远) 。另外,只有在异常时刻成功创建的对象才被清除掉。 (常
10、规函数调用则不同,它使作用域内的所有对象均被清除。 )当然,异常情况产生的对象本身在适当的地点也被清除。另外,我们可根据要求抛出许多不同类型的对象。一般情况下,对于每种不同的错误可设定抛出不同类型的对象。采用这样的方法是为了存储对象中的信息和对象的类型,所以别人可以在更大的上下文环境中考虑如何处理我们的异常。17.3 异常捕获如果一个函数抛出一个异常,它必须假定该异常能被捕获和处理。正如前文所提到的,允许对一个问题集中在一处解决,然后处理在别处的差错,这也正是 C + +语言异常处理的一个优点。17.3.1 try块如果在函数内抛出一个异常(或在函数调用时抛出一个异常) ,将在异常抛出时退出函
11、数。如果不想在异常抛出时退出函数,可在函数内创建一个特殊块用于解决实际程序中的问题(和潜在产生的差错) 。由于可通过它测试各种函数的调用,所以被称为测试块。测试块为普通作用域,由关键字t r y引导:try / code that may generate exceptions362C + +编程思想下载1 当我们运行这个例子时会惊奇地发现一些C + +编译器调用longjmp( )函数清除堆栈中的对象。这是兼容性差的问题。如果没有使用异常处理而是通过差错检查来探测错误,即使多次调用同一个函数,也不得不围绕每个调用函数重复进行设置和代码检测。而使用异常处理时不需做差错检查,可将所有的工作放入测
12、试块中。这意味着程序不会由于差错检查的引入而变得混乱,从而使得程序更加容易编写,其可读性也大为改善。17.3.2 异常处理器异常抛出信号发出后,一旦被异常器处理接收到就被销毁。异常处理器应具备接受任何一种类型的异常的能力。异常处理器紧随t r y块之后,处理的方法由关键字c a t c h引导。每一个c a t c h语句(在异常处理器中)就相当于一个以特殊类型作为单一参数的小型函数。异常处理器中标识符(i d 1、id2 等)就如同函数中的一个参数。如果异常抛出给出的异常类型足以判断如何进行异常处理,则异常处理器中的标识符可省略。异常处理部分必须直接放在测试块之后。如果一个异常信号被抛出,异
13、常处理器中第一个参数与异常抛出对象相匹配的函数将捕获该异常信号,然后进入相应的 c a t c h语句,执行异常处理程序。c a t c h语句与s w i t c h语句不同,它不需要在每个c a s e语句后加入b r e a k用以中断后面程序的执行。注意,在测试块中不同的函数的调用可能会产生相同的异常情况,但是,这时只需要一个异常处理器。 终止与恢复在异常处理原理中含有两个基本模式:终止与恢复。假设差错是致命性的,当异常发生后将无法返回原程序的正常运行部分,这时必须调用终止模式( C + +支持)结束异常状态。无论程序的哪个部分只要发生异常抛出,就表明程序运行进入了无法挽救的困境,应结
14、束运行的非正常状态,而不应返回异常抛出之处。另一个为恢复部分。恢复意味着希望异常处理器能够修改状态,然后再次对错误函数进行检测,使之在第二次调用时能够成功运行。如果要求程序具有恢复功能,就希望程序在异常处理后仍能继续正常执行程序,这样,异常处理就更象一个函数调用C +程序中在需要进行恢复的地方如何设置状态(换言之就是使用函数调用,而非异常抛出来解决问题) 。另外也可将测试块放入w h i l e循环中,以便始终装入测试块直到恢复成功得到满意的结果。过去,程序员们使用的支持恢复性异常处理的操作系统最终被终止性模式所取代,它取消了恢复性模式。所以虽然恢复性模式初听起来是十分吸引人的,但在实际运用中
15、却并非十分有效。其中一个原因可能是异常发生与异常处理相距较远的缘故。要终止相距较远的异常处理器,但是由于异常可能由很多地点产生,所以对于一个大型系统,从异常处跳转到异常处理器再跳转返回,这在概念上是十分困难的。第17章 异 常 处 理363下载17.3.3 异常规格说明可以不向函数使用者给出所有可能抛出的异常,但是这一般被认为是非常不友好的,因为这意味着他无法知道该如何编写程序来捕获所有潜在的异常情况。当然,如果他有源程序,他可寻找异常抛出的说明,但是库通常不以源代码方式提供。C + +语言提供了异常规格说明语法,我们以可利用它清晰地告诉使用者函数抛出的异常的类型,这样使用者就可方便地进行异常
16、处理。这就是异常规格说明,它存在于函数说明中,位于参数列表之后。异常规格说明再次使用了关键字t h r o w,函数的所有潜在异常类型均随着关键字 t h r o w而插入函数说明中。所以函数说明可以带有异常说明如下:void f ( ) throw ( toobig, toosmall, divzero);而传统函数声明:void f ( );意味着函数可能抛出任何一种异常。如果是:void f ( ) throw ( );这意味着函数不会有异常抛出。为了得到好的程序方案和文件,为了方便函数调用者,每当写一个有异常抛出的函数时都应当加入异常规格说明。1. unexpected( )如果函数实
17、际抛出的异常类型与我们的异常规格说明不一致,将会产生什么样的结果呢?这时会调用特殊函数unexpected( )。2. set_unexpected( )unexpected( )是使用指向函数的指针而实现的,所以我们可通过改变指针的指向地址来改变相对应的运算。这些可通过类似于 set_new_handler( )的函数set_unexpected( )来实现,set_unexpected( ) 函数可获取不带输入和输出参数的函数地址和v o i d返回值。它还返回u n e x p e c t e d指针的先前值,这样我们可存储unexpected( )函数的原先指针值,并在后面恢复它。为了
18、使用set_unexpected( )函数,我们必须包含头文件E X C E P T. H。下面给出一实例展示本章所讨论的各个特点的简单使用:364C + +编程思想下载作为异常抛出类,up 和f i t分别被创建。通常异常类均是小型的,但有时它们包含许多额外信息,这样异常处理器可通过查询它们来获得辅助信息。f( )函数在它的异常规格说明中声明函数的异常抛出只能是类 up 和 f i t,并且函数体的定义同函数的异常规格说明是一致的。函数 g( )(v e r s i o n 1)被函数f( )调用,但并不抛出异常,因此这也是可行的。当函数g( ) (v e r s i o n 1)被修改以后
19、得g( )(v e r s i o n 2) ,g( )(v e r s i o n 2)仍是f( )的调用函数,但其具有异常抛出功能。函数g( )修改以后f( )函数具有了新的异常抛出,但最初创建的f( )函数对于这些却未加声明,这样就违反了异常规格说明。my_unexpected( )函数可以没有输入或输出参数, 它是按照定制的unexpected( )函数的正确格式编写的。它仅仅打出一条有关异常的信息就退出,所以一旦被调用,我们就可以观察到这条信息。新函数unexpected( )不必有返回值(可以按照这种方法编写程序,但这是错误的) 。然而它却可抛出另一个异常(也可使它抛出同一个异常)
20、 ,或者调用函数exit( )或abort( )。如果函数unexpected( )抛出一个异常,异常处理器将在异常抛出时开始搜寻 u n e x c e p t e d异常。 (这种特点对于u n e x c e p t e d()来说是独特的)虽然new_handler( )函数的指针可为空,但unexpected( )函数的指针却不能为空。它的缺省值指向terminate( )(后面将会介绍)函数,但是,只要我们使用异常抛出和异常规格说明,我们就应该编写自己的unexpected( )函数,用于记录或者再次抛出异常及抛出新的异常或终止程序运行。在主程序中,为了对所有的潜在异常进行检测,测
21、试块被放入 f o r循环中。注意这里提到的第17章 异 常 处 理365下载实现方法很象前文介绍的恢复模式,将测试块放入f o r, while, do 或 if 的循环语句中,并利用每一个异常来试图消除差错问题;然后再一次的调用测试块对潜在异常进行检测。由于程序中f( ) 的函数声明引入了u p和f i t两类异常,因此只有该两类异常可被抛出。因为f( ) 的函数声以后要抛出的整型,所以修改后的 g( )(v e r s i o n 2)会使得函数my_unexpected( )被调用。 (我们可使用任意的异常类型,包括内部类型。 )函数set_unexcepted( )被调用后,它的返回
22、值可被忽略,但也可以被保存为函数指针,并在随后用于恢复unexcepted( )的原先指针。17.3.4 更好的异常规格说明我们可能觉得在前面介绍的已存在的异常规格说明规则并非十分可靠,并且void f ( ) ;应该意味着函数没有异常抛出,但按照前面的规则这正好相反,它表示可抛出任意类型的异常。如果程序员要抛出任意类型的异常,我们可能会想他应该说明如下void f ( ) throw (. . .); / not in C+ 因为函数声明应当更加清晰,所以这是一个改进。但不幸的是,我不能总是通过查看程序代码来知道函数是否有异常抛出例如,函数的异常抛出发生在存储分配过程中。较为糟糕的是由于调用
23、了在异常处理之前引入的函数而出现非有意的异常抛出。 (函数可能与一个新版本的异常抛出相连接)所以采用不明确的描述,如:void f ( );表示有可能有异常抛出,也可能没有。这种不明确的描述对于避免阻碍程序执行是十分必要的。17.3.5 捕获所有异常前面论述过,如果函数没有异常规格说明,任何类型的异常都有可能被函数抛出。为了解决这个问题,应创建一个能捕获任意类型的异常的处理器。这可以通过将省略号加入参数列表( la C)中来实现这一方案。catch (. . . ) cout an exception was thrown endl;为了避免漏掉异常抛出,可将能捕获任意异常的处理器放在一系列处
24、理器之后。在参数列表中加入省略号可捕获所有的异常,但使用省略号就不可能有参数,也不可能知道所接受到的异常为何种类型。17.3.6 异常的重新抛出有时需要重新抛出刚接收到的异常,尤其是在我们无法得到有关异常的信息而用省略号捕获任意的异常时。这些工作通过加入不带参数的t h r o w就可完成:catch (. . .) cout an exception was thrown endl;t h r o w ;366C + +编程思想下载如果一个c a t c h句子忽略了一个异常,那么这个异常将进入更高层的上下文环境。由于每个异常抛出的对象是被保留的,所以更高层上下文环境的处理器可从抛出来自这个
25、对象的所有信息。17.3.7 未被捕获的异常如果测试块后面的异常处理器没有与某一异常相匹配,这时内层对异常的捕获失败,异常将进入更高层的上下文环境中(高层测试块一般不最先进行异常接收) ,这个过程一直进行直到在某个层次异常处理器与该异常相匹配,这时这个异常才被认为是被捕获了,进一步的查询也将停止。假如任意层的处理器都没有捕获到这个异常,那么这个异常就是“未捕获的”或“未处理的” 。如果已存在的异常在被捕获之前又有一个新的异常产生将造成异常不能被获取,最常见的这种情况的产生原因是异常对象的构造函数自身会导致新的异常。1. terminate( )如果异常未能被捕获,特殊函数terminate(
26、)将自动被调用。如同函数unexception( )终止函数一样,它实际上也是一个指向函数的指针。在 C标准库中它的缺省值为指向函数abort( )的指针,abort( )函数可以不用调用正常的终止函数而直接从程序中退出(这意味着静态全局函数的析构函数不用被调用) 。如果一个异常未被捕获,析构函数不会被调用,则异常将不会被清除。含有未捕获的异常将被认为是程序错误。我们可将程序(如果有必要,包括 main( )的所有代码)封装在一个测试块中,这个测试块由各异常处理器按序组成,并可以捕获任意异常的缺省处理器 (catch(. . .)结束。如果我们不将程序按上述方法封装,将使我们的程序十分臃肿。一
27、个未能被捕获的异常可看成是一个程序错误。2. set_terminate( )我们可以使用标准函数set_ terminate( )来安装自己的终止函数terminate( ),set_ terminate( )返回被替代的terminate( )函数的指针,这样就可存贮该指针并在需要时进行恢复。定做的终止函数 terminate( )必须不含有输入参数,其返回值为v o i d。另外所安装的任何终止处理器terminate( )必须不返回或抛出异常,但是作为替换将调用一些程序终止函数。在实际中如果函数terminate( )被调用就意味着问题将无法被恢复。如同函数unexpected( )一
28、样,函数terminate( )的指针不能为零。这儿给出一实例用以展示set_ terminate( )的使用。例中set_ terminate( )函数被调用后返回函数terminate( )的原先的指针,存储该指针并为以后的恢复做准备,这样可通过函数 t e r m i n a t e ()为判断未捕获的异常在程序中何处发生提供帮助:第17章 异 常 处 理367下载o l d _ t e r m i n a t e的定义初看上去有些令人费解:该语句不仅创建了一个指向函数的指针o l d _ t e r m i n a t e,而且将其初始化为函数set_ terminate( )的返回值
29、。虽然我们可能比较熟悉在函数指针后面加分号的定义方法,但例中所给出是另一种变量并可在定义时进行初始化。类b o t c h不仅在函数f( )内部会抛出异常,而且在它的析构函数内也会抛出异常。从主程序中可见,这是调用函数terminate( )的一种情况。虽然异常处理器中使用了c a t c h ( . . . )函数,从表面上看它似乎可以捕获所有的异常,避免函数 terminate( )的调用,但是当处理一个异常需清除堆栈中的对象时,在这一过程中将调用类b o t c h的析构函数,由此产生了第二个异常,这将迫使函数terminate( )被调用。因此析构函数中含有异常抛出或引起异常抛出都将是
30、一个设计错误。17.4 清除异常处理的部分难度就在于异常抛出时从正常程序流转入异常处理器中。如果异常抛出时对象没有被正确地清除,这一操作将不会很有效。 C + +的异常处理器可以保证当我们离开一个作用域时,该作用域中所有结构完整的对象的析构函数都将被调用,以清除这些对象。这里给出一个例子,用以演示当对象的构造函数不完整时其析构函数将不被调用,它也用来展示如果在被创建对象过程中发生异常抛出时将出现什么结果,如果 unexpected( )函数再次抛出意外的异常时将出现什么结果:368C + +编程思想下载第17章 异 常 处 理369下载类n o i s y可跟踪对象的创建,所以可通过它跟踪程序
31、的运行。类 n o i s y中含有静态整数变量i用以记录创建对象的个数,整数变量 o b j n u m用以记录特殊对象的个数,字符缓冲器 n a m e用以保存字符标识符。该缓冲器首先设置为零,然后把构造函数的参数拷贝给它。 (注意这里用缺省的字符串参数表明所创建的为数组元素,所以该构造函数实际上充当了缺省构造函数。 )因为C标准库中函数strncpy( )在它的第三个参数指定的字符数出现或零终结符出现时,将终止字符的复制,所以被复制字符的数肯定小于缓冲器的大小,并且最后一个字符始终为零,因此打印语句将决不会超出缓冲器。构造函数在两种情况下会发生异常抛出。第一种情况是当第五个对象被创建时(
32、这只是为了显示在对象数组创建中发生异常,而不是真正的异常条件) ,这种异常将抛出一个整数,并且函数在异常规格说明中已引入了整数类型。第二种情况当然也是特意设计的,当参数字符串的第一个字符为“z”时将抛出一字符型异常。由于异常规格说明中不含有字符型,所以这类异常将调用unexpected( )函数。函数new 和 d e l e t e可对类进行重载,其功能可见其函数调用。函数unexpected_rethrow( ) 打印一条信息,并且再次抛出同一个异常。在主程序 main( )的第一行中,它充当unexpected( )函数被安装。在测试块中将创建一些n o i s y对象,但是在对象数组的
33、创建中有异常抛出,所以对象n 2将不会被创建。这些在程序输出结果中可以见到:370C + +编程思想下载程序成功地创建了四个对象数组单元,但在构造第五个对象时发生异常抛出。由于第五个对象的构造函数未完成,因此异常在清除对象时只有1 4的析构函数被调用。全局函数n e w的一次调用所产生的对象数组的存储分配是分离的。注意,即使程序中没有明确地调用函数d e l e t e,但异常处理系统仍不可避免地调用d e l e t e函数来释放存储单元。只有在使用规范的n e w函数形式时才会出现上述情况。如果使用第 1 2章介绍的语法,异常处理机构将不会调用d e l e t e函数来清除对象,因为它只
34、适用于清除不是堆结构的存储区。最终对象n 1将被清除,而对象n 2由于没被创建所以也不存在被清除的问题。在测试函数unexpected_rethrow( )的程序段中,对象n 3已被创建,对象n 4的构造函数已开始创建对象。但是在它创建完成之前已有异常抛出。该异常为字符型,不存在于函数的异常规格说明中,所以函数unexpection( )将被调用(在此例中为函数unexpected_rethrow( )) 。由于函数unexpected_rethrow( )可抛出所有类型的异常,所以该函数将再次抛出与已知类型完全相同的异常。当对象n 4的构造函数被调用抛出异常后,异常处理器将进行查找并捕获该异
35、常(在成功创建的对象n 3被清除之后) 。这样函数unexpected_rethrow( )的作用就是接收任意的未加说明的异常,并作为已知异常再次抛出;使用这种方法该函数可为我们提供一滤波器,用以跟踪意外异常的出现并获取该异常的类型。17.5 构造函数当编写的程序出现异常时,我们总会问: “当异常出现时,这能被合理地清除掉吗?”这是十分重要的。对于大多数情况,程序是相当安全的;但是如果构造函数中出现异常,这将产生问题:如果异常抛出发生在构造函数创建对象时,对象的析构函数将无法调用其相应的对象。这意味着在编写构造函数的程序时必须十分谨慎。构造函数进行存储资源分配时存在普遍的困难。如果构造函数在运
36、行时有异常抛出,析构函数将无法收回这些存储资源。这些问题大多数发生在未加保护的指针上。例如:第17章 异 常 处 理371下载输出是:当进入类u s e R e s o u r c e s的构造函数后,并且类b o n k的构造函数已成功地完成了对象数组的创建,而这时,在og:operator new中抛出一个异常(例如存储耗尽所产生的异常) 。这样我们就意外地在异常处理器中结束程序,而 u s e R e s o u r c e s所对应的析构函数未得到调用。这是正常的,因为类u s e R e s o u r c e s的构造函数的全部构造工作没能全部完成,这就意味着基于堆存储的类b o
37、n k的对象也不能被析构。对象化为了防止上文提到的情况,应避免对象通过本身的构造函数和析构函数将“不完备的”资源分配到对象中。利用这种方法,每个分配就变成了原子的,像一个对象,并且如果失败,那372C + +编程思想下载么已分配资源的对象也被正确地清除。采用模板是修改上例的一个好方法:第17章 异 常 处 理373下载不同点是使用模板封装指针并将它送入对象。这些对象的构造函数的调用先于u s e R e s o u r c e s构造函数的调用,如果这些构造函数的创建操作完成之后发生了异常抛出,与它们相对应的析构函数被调用。模板p w r a p演示了比前面所见更为经典的异常使用:如果 o p
38、 e r a t o r 的参数出界,那么就创建一个嵌入类r a n g e E r r o r用于o p e r a t o r 中。因为o p e r a t o r 返回一个引用而不是返回0。 (没有0引用。 )这是一个真实的异常情况:不知道在当前上下文中该做什么,也不能返回一个不可能的值。在此例中,r a n g e E r r o r很简单而且设想所有必须的信息都在类名中,但我们也可加入含有索引值的成员,如果这样做有用的话。现在输出是:对o g的空间存储分配又抛出一个异常,但这次b o n k对象数组正确地被清除,所以没有存储损耗。374C + +编程思想下载17.6 异常匹配当一个
39、异常抛出时,异常处理系统会根据所写的异常处理器顺序找到“最近”的异常处理器,而不会搜寻更多的异常处理器。异常匹配并不要求在异常和处理器之间匹配得十分完美。一个对象或一个派生类对象的引用将与基类处理器匹配(然而假若处理器针对的是对象而非引用,异常对象在传递给处理器时会被“切片” ,这样不会受到破坏但会丢失所有的派生类型信息) 。假若抛出一个指针,标准指针转化处理会被用于匹配异常,但不会有自动的类型转化将某个异常类型在匹配过程中转化为另一个。下面是一个例子:尽管我们可能认为第一个处理器会使用构造函数转化,将一个 e x c e p t 1对象转化成e x c e p t 2对象,但是系统在异常处理
40、期间将不会执行这样的转换,我们将在 e x c e p t 1处终止。下面的例子展示基类处理器怎样捕获派生类的异常:第17章 异 常 处 理375下载这里的异常处理机制,对于第一个处理器总是匹配一个 t r o u b l e对象或从t r o u b l e派生的什么事物,由于第一个处理器捕获涉及第二和第三处理器的所有异常,所以第二和第三处理器永远不被调用。光捕获派生类异常把基类的一般异常放在末端捕获更有意义(或者在随后的下一个开发周期中引入的派生类) 。另外,假若s m a l l和b i g的对象比t r o u b l e的大(这常常是真实的,因为通常为派生类添加成员) ,那么这些对象
41、会被“切片”以适应处理器。当然,在本例中由于派生类没有附加成员,而且在处理器中也没有参数标识,所以这一点并不重要。通常在处理器中,应该使用引用参数而非对象以避免裁剪掉信息。17.7 标准异常用于C + +类标准库的一批异常可以用于我们自己的程序中。从标准异常类开始会比我们尽量自己定义来得快和容易。假若标准异常类不能满足需要,我们可以继承它并添加自己的特定内容。下面的表描述了标准异常:e x c e p t i o n是所有标准C + +库异常的基类。我们可以调用w h a t ( )以获得其特性的显示说明l o g i c _ e r r o是由e x c e p t i o n派生的。它报告
42、程序的逻辑错误,这些错误在程序执行前可以被检测到r u n t i m e _ e r r o r是由e x c e p t i o n派生的。它报告程序运行时错误,这些错误仅在程序运行时可以被检测到I / O流异常类i o s : : f a i l u r e也由e x c e p t i o n派生,但它没有进一步的子类:下面两张表中的类都可以按说明使用,也可以作为基类去派生我们自己的更为特殊的异常类型。由l o g i c _ e r r o r派生的异常d o m a i n _ e r r o r报告违反了前置条件i n v a l i d _ a rg u m e n t指出函数
43、的一个无效参数l e n g t h _ e r r o r指出有一个产生超过N P O S长度的对象的企图(N P O S:类型size_t 的最大可表现值)o u t _ o f _ r a n g e报告参数越界b a d _ c a s t在运行时类型识别中有一个无效的 d y n a m i c _ c a s t表达式(见第1 8章)b a d _ t y p e i d报告在表达式t y p e i d ( * p )中有一个空指针P(运行时类型识别的特性见第1 8章)由r u n t i m e _ e r r o r派生的异常r a n g e _ e r r o r报告违反
44、了后置条件o v e r f l o w _ e r r o r报告一个算术溢出b a d _ a l l o c报告一个存储分配错误376C + +编程思想下载17.8 含有异常的程序设计对大多数程序员尤其是C程序员,在他们的已有的程序设计语言中不能使用异常,需进一步矫正。下面是一些含有异常的程序设计原则。17.8.1 何时避免异常异常并不能回答所发生的所有问题。实际上若对异常进行钻牛角尖式的推敲,将会遇到许多麻烦。下面的段落指出异常不能被保证的情况。1. 异步事件标准C的s i g n a l ( )系统以及其他类似的系统操纵着异步事件:该事件发生在程序控制的范围以外,它的发生是程序所不能
45、预计的。由于异常和它的处理器都在相同的调用栈上,所以异常不能用来处理异步事件。也就是异常限制在某范围内,而异步事件必须有完全独立的代码来处理,这些代码不是普通程序流的一部分(典型的如中断服务和事件循环例程) 。这并不是说异步事件不能和异常发生关系。但是,中断服务处理器都尽可能快地工作,然后返回。在一些定义明确的程序点上,一个异常可以以基于中断的方式抛出。2. 普通错误情况假若有足够的信息去处理一个错误,这个错误就不是一个异常。我们应该关心当前的上下文环境,而不应该把异常抛向更大的上下文环境中。同样,在 C + +中也不应当为机器层的事件抛出异常,如“除零溢出” 。可以认为这些“异常”可由其他的
46、机制去处理,如操作系统或硬件。这样,C + +异常可以相当有效,并且它们的使用和程序级的异常条件相互隔离。3. 流控制一个异常看上去有点象一个交替返回机制,也有点象一个 s w i t c h语句段,我们可能被它们吸引,改变了想使用它们的初衷,这是一个很糟糕的想法,一部分原因是因为异常处理系统比普通的程序运行缺乏效率。异常是一个罕有的事件,所以普通程序不应为其支付时间,来自非错误条件的其他什么地方的异常也会给使用我们的类或函数的用户带来相当的混乱。4. 不强迫使用异常一些程序相当简单,如一些实用程序,可能仅仅需要获取输入和执行一些加工。如果在这类程序中试图分配存储然而失败了,或打开一个文件然而
47、失败了等等,这样可以在这类程序中使用a s s e r t ( )以示出错信息,使用a b o r t ( )终止程序,允许系统清除混乱。但是如果我们自己努力去捕获所有异常,修复系统资源,则是不明智的。从根本上说,假若我们不必使用异常,我们就不要用。5. 新异常,老代码另一种情形出现在对没有使用异常的已存在的程序进行修改的时候。我们可能引入一个使用异常的库而且想知道是否有必要在程序中修改所有的代码。假定已经安放了一个可接受的出错处理配置,这里所要做的最明智的事情是围绕着使用新类t r y块的最大程序块,追加一个c a t c h ()和基本出错信息。我们可以追加有必要的更多特定的处理器,并使修
48、改更为细致。但是,在这种情况下,被迫增加的代码必须是最小限度的。我们也可以把我们的异常生成代码隔离在t r y块中,并且编写一个把当前异常转换成已存在的出错处理方案的处理器。创建一个为其他人使用的库,而且无从知晓用户在遭遇决定性错误的情况下如何反应,这时考虑异常才真正重要。第17章 异 常 处 理377下载17.8.2 异常的典型使用使用异常便于:1) 使问题固定下来和重新调用这个(导致异常的)函数。2) 把事情修补好而继续运行,不去重试函数。3) 计算一些选择结果用于代替函数假定产生的结果。4) 在当前上下文环境尽其所能并且再把同样的异常弹向更高的上下文中。5) 在当前上下文环境尽其所能并且
49、把一个不同的异常弹向更高的上下文中。6) 终止程序。7) 包装使用普通错误方案的函数(尤其是C的库函数) ,以便产生异常替代。8) 简化,假若我们的异常方案建造得过于复杂,使用时会令人懊恼。9) 使我们的库和程序更安全。这是短期投资(为了调试)和长期投资(为了应用的健壮性)问题。1. 随时使用异常规格说明异常的规格说明像一个函数原型:它告诉用户书写异常处理代码以及处理什么异常。它告诉编译器异常可能出现在这个函数中。当然,我们不能总是通过检查代码而预见什么异常会发生在特定的函数中。有时这个特定函数所调用的函数产生了一个出乎意料的异常,有时一个不会抛出异常的老函数被一个会抛出异常的新函数替换了,这
50、样我们将产生对 u n e x p e c t ( )的调用。无论何时,只要使用异常规格说明或者调用含有异常的函数,都应该创建自己的 u n e x p e c t e d ( )函数,该函数记录信息而且重新抛出同样的异常。2. 起始于标准异常在创建我们自己的异常前应检查标准 C + +异常库。假若标准异常正合所需,则这样会使我们的用户更易于理解和处理。假若所需要的异常类型不是标准库的一部分,则尽量从某个已存在标准 e x p e c t i o n中派生形成。假若在e x p e c t i o n的类接口中总是存在w h a t ( )函数的期望定义,这会使用户受益匪浅。3. 套装我们自己
51、的异常如果为我们的特定类创建异常,在我们的类中套装异常类是一个很好的主意,这为读者提供了一个清晰的消息这些异常仅为我们的类所使用。另外,它可防止命名域混乱。4. 使用异常层次异常层次为不同类型的重要错误的分类提供了一个有价值的方法,这些错误可能会与我们的类或库冲突。该层次可为用户提供有帮助的信息,帮助他们组织自己的代码,让他们可以选择是忽略所有异常的特定类型还是正确地捕获基类类型。而且在以后,任何异常可通过对相同基类的继承而追加,而不会被迫改写所有的已生成代码基类处理器将捕获新的异常。当然,标准C + +异常是一个异常层次的优秀例子,通过使用可进一步增强和丰富它。5. 多重继承我们会记得,在第
52、1 5章中,多重继承最必要做的地方就是需要把一个指向对象的指针向上映射到两个不同的基类,也就是需要两个基类的多态行为的地方。这样,异常层次对于多重继承是有用的,因为多重继承异常类的任一根的基类处理器都可处理异常。6. 用“引用”而非“值”去捕获如果抛出一个派生类对象而且该对象被基类的对象处理器通过值捕获到, 对象会被 “切片” ,378C + +编程思想下载这就是说,随着向基类对象的传递,派生类元素会依次被割下,直到传递完成。这样的偶然性并不是所要的,因为对象的行为像基类而不象它本来就是的派生类对象(实际就是“切片”以前) 。下面是一个例子:当对象通过值被捕获时,因为它被转化成一个b a s
53、e对象(由构造函数完成),而且在所有的情况下表现出b a s e对象的行为;然而当对象通过引用被捕获时,仅仅地址被传递而对象不会被切片,所以它的行为反映了它处于派生中的真实情况。虽然也可以抛出和捕获指针,但这样做会引入更多的耦合抛出器和捕获器必须为怎样分配和清理异常对象而达成一致。这是一个问题,因为异常本身可能会由于堆的耗尽而产生。如果抛出异常对象,异常处理系统会关注所有的存储。第17章 异 常 处 理379下载输出为7. 在构造函数中抛出异常由于构造函数没有返回值,因此在先前我们可以有两个选择以报告在构造期间的错误:1) 设置一个非局部标志并且希望用户检查它。2) 返回一个不完全被创建的对象
54、并且希望用户检查它。这是一个严重的问题,因为 C程序员必须依赖一个隐含的保证:对象总是成功地被创建,这在类型如此粗糙的C中是不合理的。但是在C + +程序中,构造失败后继续执行是注定的灾难,于是构造函数成为抛出异常最重要的地方之一。现在有一个安全有效的方法去处理构造函数错误。然而我们还必须把注意力集中在对象内部的指针上和构造函数异常抛出时的清除方法上。8. 不要在析构函数中导致异常由于析构函数会在抛出其他异常时被调用, 所以永远不要打算在析构函数中抛出一个异常,或者通过执行在析构函数中的相同动作导致其他异常的抛出。如果这些发生了,这意味着在已存在的异常到达引起捕获之前抛出了一个新的异常,这会导
55、致对 t e r m i n a t e ( )的调用。这里的意思是:假若调用一个析构函数中的任何函数都有可能会抛出异常,这些调用应该写在析构函数中的一个t r y块中,而且析构函数必须自己处理所有自身的异常。这里的异常都不应逃离析构函数。9. 避免无保护的指针请看第1 7 . 5 . 1节中的W R A P P E D . C P P程序,假若资源分配给无保护的指针,那么意味着在构造函数中存在一个缺点。由于该指针不拥有析构函数,所以当在构造函数中抛出异常时那些资源将不能被释放。17.9 开销为了使用新特性必然有所开销。当异常被抛出时有相当的运行时间方面的开销,这就是从来不想把异常用于普通流控
56、制的一部分的原因,而不管它多么令人心动。异常的发生应当是很少的,所以开销聚集在异常上而不是在普通的执行代码上。设计异常处理的重要目标之一是:在异常处理实现中,当异常不发生时应不影响运行速度。这就是说,只要不抛出异常,代码的运行速度如同没有加载异常处理时一样。无论与否,异常处理都依赖于使用的特定编译器。异常处理也会引出额外信息,这些信息被编译器置于栈上。除了能作为特定的“异常范围” (它可能恰恰是全局范围)的对象传进送出外,异常对象可以像其他对象一样被正确地在周围传递。当异常处理器工作完成时,异常对象也被相应地销毁。17.10 小结错误恢复是和我们编写的每个程序相关的基本原则,在 C + +中尤
57、其重要,创建程序组件为其他人重用是开发的目标之一。为了创建一个稳固系统,必须使每个组件具有健壮性。C + +中异常处理的目标是简化大型可靠程序的创建,使用尽可能少的代码,使应用中没有不受控制的错误而使我们更加自信。这几乎不损害性能,并且对其他代码的影响很小。基本异常不特别难学,我们应该在程序中尽量地使用它们。异常是能给我们提供即时而显著的好处的特性之一。17.11 练习1) 创建一个含有可抛出异常的成员函数的类。在该类中,创建一个被嵌套的类用作一个异380C + +编程思想下载常对象,它带有一个c h a r *参数,该参数表示一个描述型字符串。创建一个可抛出该异常的成员函数。 (标明函数的异
58、常规格说明)书写一个 t r y块使它能调用该函数并且捕获异常,以打印描述型字符串的方式处理该异常。2) 重写第1 2章中的s t a s h类以便为o p e r a t o r 抛出o u t - o f - r a n g e异常。3) 写一个一般的m a i n ( ),它可取走所有的异常并且报告错误。4) 创建一个拥有自身运算符n e w的类。该运算符分配1 0个对象,在对第11个对象分配时假定“存储耗尽”并抛出一个异常。增加一个静态函数用于回收存储。现在,创建一个伴有 t r y块和能够调用存储恢复例程的 c a t c h子句的主程序,将这些都放入一个 w h i l e循环中,
59、演示异常恢复和连续执行的情形。5) 创建一个可抛出异常的析构函数,编写代码以向自己证明这是一个糟糕的想法。该代码可展示如果处理器对一个已存在异常施加影响之前一个新异常又抛出了,那么 t e r m i n a t e ( )会被调用。6) 向我们自己证明所有的异常对象(被抛出的)都能被正确地销毁。7) 向我们自己证明假若我们在堆上创建一个异常对象并且抛出一个指向该对象的指针,则它不会被清理掉。8) (高级) 。使用一个带有构造函数和拷贝构造函数的类来追踪异常的创建和传递,这些构造函数和拷贝构造函数显示它们自身而且尽可能地提供关于对象是怎样创建的信息(就拷贝构造函数而言,说明创建什么对象) 。创立一个有趣的状态,抛出我们的新类型的对象并分析结果。第17章 异 常 处 理381下载