C++语言程序设计 9第九讲——异常处理_第1页
C++语言程序设计 9第九讲——异常处理_第2页
C++语言程序设计 9第九讲——异常处理_第3页
C++语言程序设计 9第九讲——异常处理_第4页
C++语言程序设计 9第九讲——异常处理_第5页
已阅读5页,还剩71页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第十二章 异常处理,C+语言程序设计,本章主要内容,异常处理的基本思想 C+异常处理的实现 异常处理中的构造与析构,编译错 编译时通不过,属语法错,最浅层次的错误。 逻辑错 设计缺陷,编译器无法发现,只能靠人工分析跟踪排除,错误层次中等。 运行错 调试时无法发现,运行时才出现,往往由系统环境引起,属可预料但不可避免。必须由语言的某种机制予以控制。 注意,调试出错和运行出错时机不同,是两种不同性质的错误。,错误分类:,程序逻辑中不应出现的情况叫“非法”。它不应包含在正常程序代码中。如对一个字符串拷贝函数传入空串指针,调试时要用assent()拦截。 在程序运行中出现的、属正常逻辑,但一旦出现会导致程序瘫痪,叫“出错”。比如在读某文件之前,要先打开文件,打开失败不能算“非法”,只算“出错”。,分清“非法”与“出错”,一天,某公司派小张去机场提货,派小王去银行存款。 机场仓库失火,小张没提出货,也没回公司报告,原来一根筋的他在一边看热闹,可公司上下急得团团转。 在去银行路上,小王遇到抢匪打劫,小王立即报警,并报告了公司。马上协助警方追捕抢匪,追回存款,再送银行完成了存款任务。 假设,你是公司经理,你需要什么样的员工?,为何需要“异常处理”,对于可预料但不可避免的程序错误,不能眼睁睁看着它发生而无所作为(只会用exit()),要将消极等待变为积极预防,还要将预防的处理内容归纳整理,分门别类地作成类。 积极之意是,不能只凭编程经验这种个体性偶然性的做法,而要凭借人人都掌握的规范圆满的处理。 这规范是指在函数的处理中设下陷阱,一旦触发异常,定会被异常处理所收容,统一归口处理。,异常处理的指导思想,默认的 程序中断执行。 特制的用“异常处理” 转而执行特制的处理函数 。,错误处理的做法:,两个人不幸得了病,他们得到不同的处置: exit() 不分青红皂白,马上杀了埋掉。 “异常处理” 马上送去医院就诊,尽力治疗,可能痊愈出院,也可能经全力抢救还是无力回天,撒手人间 。 两种做法,孰优孰劣,一目了然。,exit() 与异常处理的比较,允许从异常抛出点把任意数量的信息以类型安全的方式传给异常处理器(catch); 对于没有抛出异常的代码段,不应有任何额外的系统(时间、空间)开销; 应保证所抛出的任何异常都能被适当的处理器捕获; 通过语言提供的某种语法,程序员可以写出自己的异常处理器; 能够直接应用到多线程程序中; .,异常处理的追求目标 Stroustrop,异常处理机制实际上是一种运行时通知机制,而非程序预设机制。 异常处理机制发生在错误出现之前(异常条件一旦符合,立即触发“抛掷”)。 而C语言的出错处理机制则是,当错误发生之后所采取的补救/罢工措施。 防患于未然,成功避险,显然优于事后补救。 一个大型软件,其功能是通过底层调用来实现的,错误大都发生在底层。你总不能仅仅提示“数据溢出”、“进程失败”之类的信息。而应当“翻译”成“账户余额不足,不能转帐”、“燃油不足,发动机即将关闭”之类。而上层组件如何知道下层发生了什么错误?当然要底层代码的报告,报告的方法就是抛出异常对象。,由于c/c+是函数性语言,函数的调用是嵌套的,异常的发生地和处理地就很可能不在同一个模块中,逻辑上讲应是嵌套的; 异常一旦发生,则意味着后续语句不可能再执行了。,异常处理的基本认识,如有一函数其调用式为: sin ( 2a/sqrt(x+y)/c),sin,sqrt,2*a,x+y,/c,抛掷错 (拦截),域,异常处理的基本思想,异常处理的实现机制,抛掷异常的程序段 throw 表达式; ,捕获并处理异常的程序段 try 复合语句 catch(异常类型声明) 复合语句 catch(异常类型声明) 复合语句 ,相呼应的三个关键字:try throw catch,这三个关键字分工明确,各司其职: throw负责发现异常的报警、抛掷异常对象;若放在函数声明中则又称为异常接口声明; try设置了一个侦错范围,又叫保护段,是个区域。其实是划定了一个跳跃的边界; catch负责处理捕获来的异常(包括继续抛掷异常)。,异常处理的语法结构,每个catch()相当于一段函数代码; 每个throw则相当于一个函数调用; 每个try块至少跟一个catch(); 一个程序可设置个数不定的try 、throw 和 catch。它们只有逻辑上的呼应,而无数量上的对应关系,且不受所在函数作用域限制; 异常抛掷点往往距异常捕获点很远,它们可以不在同层模块中; 甚至有的throw我们看不到在哪,实际上在我们所调用的系统函数中,在标准库中; 程序中try块可以并列、可以嵌套; catch子句的类型匹配靠C+的RTTI技术。,throw和catch的呼应机制是靠类型匹配实现的。 throw只管抛出异常的类型,至于由哪个catch接着,那是匹配机制的事。 由于异常处理机制是按类型匹配的,因此catch的参数可以没有参数名,除非你要使用参数做别的事情。而且只能有一个参数。 是将检测与处理分离,以便各司其职、灵活搭配地工作。它们的联系靠类型。 任何类型都可以作为异常类型,包括基本类型或扩展类型,甚至空类型。 所抛掷的异常对象并非建在函数栈上,而是建在专用的异常栈上,故可以跨越函数而传递给上层。因此也不要将函数的局部对象的地址或引用作为异常对象抛出,因为局部对象将随着函数栈的清除而消失。,异常处理机制的图示,17,普通函数,catch,try,throw,main(),try块内函数,try,catch,异常处理的实现机制,若有异常则通过throw创建一个异常对象并抛掷。 将可能抛出异常的程序段嵌在try块之中。按正常的顺序执行到达try语句,然后执行try块内的保护段。 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。 catch子句按其在try块后出现的顺序被匹配。选中的catch将捕获并处理异常(或继续抛掷异常)。 如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。,例12-1处理除零异常,#include int Div(int x,int y) if(y=0) throw y; return x/y; void main() try cout“5/2=“Div(5,2)endl; cout“8/0=“Div(8,0)endl; cout“7/1=“Div(7,1)endl; catch(int) cout“except of deviding zero.n“; cout“that is ok.n“; ,程序运行结果如下: 5/2=2 except of deviding zero. that is ok.,例 除零异常的类实现,#include class DevidedByZero public: DevidedByZero (const char * p ); const char * description(); / . private: char * desp; ; double Devide ( double a, double b) if( b= 0 ) throw DevidedByZero( “The divisor is 0.”); return a/b; ,void main() double x = 100,y = 20.5; try cout“100 / 20.5=“Devisor(x,y)endl; catch(const DevidedByZero 此例,是定义一个异常类,来具体描述所需要的异常类型,来保存异常信息的。以弥补基本类型的能力不足和信息不足。,程序运行结果如下: that is ok.,C+中对抛出的异常是按排列次序匹配的。catch处理程序的出现顺序很重要,因为在一个try块中,异常处理程序是按照它出现的顺序被检查的。只要找到一个匹配的异常类型,后面的异常处理都将被忽略。例如,在下面的异常处理块中,首先出现的是catch(.),它可以匹配任何异常,在任何情况下,其它的catch语句都被略过。因此,catch(.)应该放在最后。,catch的匹配次序,/. try /. catch(.) /只在这里处理所有的异常 /错误:后面的异常处理程序段不会被检查到 catch(const char* str) ,关于throw抛掷的异常对象,若throw抛掷了异常对象,则该对象应该是非局部对象本身,也非该对象的引用,并且非指向该对象的指针。 它应该是一个叫做异常对象的东西。因为局部对象是短命的。,trythrowcatch实际是一种程序控制机制,当其中的throw语句一旦被触发,就会将执行流引到其后的catch去匹配。只要找到一个匹配的异常类型,其它的异常处理都将被忽略。当异常处理块被执行完后,会找到本层catch之后的语句去执行,try中被throw 跳过的语句不会再被执行。 这种机制与程序三大控制结构很类似,只不过是非正常情况下的一种控制机制。,异常处理的语法机制,与异常处理相关的四个流程,1. 没有异常抛出时的正常执行流程; 2. 抛掷了异常,且在当前函数范围内被捕获着了的执行流程; 3. 抛掷了异常,但在当前函数范围内没有被捕获着的执行流程; 4. 异常被处理后的执行流程。,C对出错的处理是将所有错误予以编码(返回码),一码一错。程序员编程时,通过在主调函数中检查出现的返回码,给与相应的处理。这种方法繁琐又不规范,更致命的是,这种“事后处理”的机制会使一切抢救都为时已晚。 C+则是将形形色色的错误归结抽象为“类型错”,统统交给throw抛掷;将出错处理统一为catch 调用。这就为程序员编程制定了统一的规范,也就是运行协议。更重要的是, C+的出错处理机制是“拦截”,然后做抢救处理。能抢救过来,则救活了,因此属“防患于未然”。 “异常处理”实际是个动态概念。其处理机制是靠类型匹配,这恰是运行时才能发生的事。,C+的异常处理机制有何特色?,C对出错处理的返回码机制 ,与C+的异常处理机制有何不同? 1。C将正常逻辑与异常逻辑混在一起。每个函数调用后都要用正常逻辑(如if)加以判断,以过滤出异常。这样使子函数的错误扩散到了其所有的调用者中,形成了一个混杂的怪异链。 2。增加了用于处理很少出现的特殊情况的运行费用。尤其对于C+,其成员函数通常只有几行代码,但函数很多,若每个函数都使用返回码技术,则臃肿不堪。 3。类中还有构造、拷贝构造等无返回值的函数,它们出错处理的权力不应被剥夺。 4。返回码靠的是预定的数字代号来传递信息,所能携带的信息量少且呆板,远少于抛掷的对象所携带的信息量。,异常处理的不唤醒机制,即一旦在保护段执行期间发生异常,则立即throw抛掷,由相应的catch子句捕获处理。抛掷 捕获之间的代码被越过而不执行。程序从try块后跟随的catch子句最后一个后面的语句继续执行下去。 throw越过的语句不再执行;catch执行之后也不再返回。 异常处理是一种C+新增的程序控制机制(还记得原有的九种控制机制吗?),也不同于函数调用机制(函数属唤醒机制)。当不唤醒机制与以上其他机制交织在一起时,具有最高的优先级。,C+对异常处理的保证,如果抛掷的异常在所在的模块层次没有得到处理,即没有找到类型匹配的处理器,则被上溯到调用层,继续寻找,这个过程不断进行,直至main函数,若还没找到则调用terminate函数来结束程序。 catch叫异常处理器(handler)。一个catch只能捕获一个类型的异常;一个throw也只能抛掷一种类型的异常; catch 必须与try捆绑在一起,而throw则可以远离try结构。,异常处理中的构造与析构,一旦异常发生,会生成一个exception object并放入exception栈中; 搜索、找到一个匹配的catch异常处理: 用exception object初始化catch的参数(可能要拷贝该异常对象,也可能仅传址); 将从对应的try块开始到异常被抛掷处之间已构造(且尚未析构)的所有对象进行析构; 执行catch函数体; 从最后一个catch处理之后的语句恢复执行。,例12-2 异常处理时的析构,#include class Expt public: Expt()cout “构造了 Expt.“ endl; Expt(Expt ,class Demo public: Demo() cout“构造 Demo.“endl; Demo() cout“析构 Demo.“endl; ; void MyFunc() Demo D; cout“在MyFunc()中抛掷Expt类异常。“endl; throw Expt(); ,33,void main() cout“在main函数中。“endl; try cout“在try块中,调用MyFunc()。“ endl; MyFunc(); catch( Expt E ) cout“在catch异常处理程序中。“endl; cout“捕获到Expt类型异常:“; coutE.ShowReason()endl; catch( char *str ) cout“捕获到其它的异常:“strendl; cout“回到main函数。从这里恢复执行。“endl; ,34,程序运行时输出: 在main函数中。 在try块中,调用MyFunc()。 构造 Demo. 在MyFunc()中抛掷Expt类异常。 构造了 Expt. 拷贝构造了 Expt.“ 析构 Demo. 在catch异常处理程序中。 捕获到Expt类型异常:Expt类异常。 析构了 Expt. 析构了 Expt. 回到main函数。从这里恢复执行。,35,注意,本例中,两个catch处理器中都说明了异常参量: catch(Expt E) /. catch(char* str) /. 其实,也可以不说明这些参量(E和str)。在很多情况下,只要通知处理程序已经产生有某个特定类型的异常就足够了。但在需要访问异常对象时就要说明参量,否则,将无法在catch处理程序中访问那个对象。例如: catch(Expt) / 只有类型,没有对象 /在这里不能访问Expt异常对象 ,匹配规则,当异常对象和catch子句的参数类型符合以下条件时,匹配成功: (次序自上而下) 若catch子句的参数类型就是抛掷出来的异常对象的类型时; 若catch子句的参数类型是抛掷出来的异常对象类型的公共基类时; 若catch子句的参数类型是基类的指针或引用,而抛掷出来的异常对象恰为其派生类的指针或引用时; 若catch子句的参数类型是void *,而抛掷出来的异常对象为任何类型的指针时; 若catch子句的参数是catch all时,即catch(.) 。,异常处理机制的承诺,从try以来,到发生throw时,这期间所有构造的局部非静态对象的析构函数将被自动调用,然后清退函数的运行栈。这是个上溯过程,直到找到包含throw的try块为止。 如果一直上溯到main还没找到该与之呼应的catch子句,则系统会调用terminate()来终止程序,此时将不能保证所有的局部非静态对象都将被自动销毁。,对象创建期的异常,“对象创建期”显然指的是构造函数。由于它没有返回值,所以一旦出错,最好启用异常机制。 对于静态创建的对象,只要将其罩在try块中即可。 对于动态创建对象(使用new) ,将如何? 例如 T *p = new T ; 将被编译器变成类似下面的样子: T* p = reinterpret_cast(_new charsizeof T); try new (p) T ; catch ( . ) delete p;/释放刚得到的内存 throw ; / 二次抛掷 ,首先分配原始内存(不调用构造函数),若失败则抛出bad_alloc异常。,然后用“放置内存”的方法,使用已分配的内存,课堂练习:读代码,分析运行结果,解释原因。 void f(); class T public: T() cout“constructor”endl; try throw “exception 1”; catch(char *) cout “exception 1”endl; throw “exception”; T() cout “destructor”; ;,40,/2 构造函数中含有异常处理,/ 将被执行,/ 3,/ 不被执行,why?,void main() cout“main function”endl; try f() ; catch(char *) cout “exception 2”endl; cout“main function”endl; void f() T t; ,41,/ 5,/ 4,/ 1,结论: 因构造函数中含有异常处理,对象构造未完成,故析构函数没有必要运行。,编译器应具有这样的功能:若构造函数在调用时发生了异常,从而导致执行中断,不应引起内存泄漏。 这就要求编译器做出判断: 若内存尚未分配,则直接中断执行; 若内存已经分配,则要释放所得到的内存后再中断。 由于存在这种机制,因此建议编程者:最好将“资源”做成类。在下例1,2处发生异常怎么办? void mumble(void * arena) Point *p = new Point; smLock ( arena ); . smUnLock( arena ); delete p; ,42,1,2,将前例改为: void mumble(void * arena) auto_ptr ph ( new Point); SMLock sm ( arena ); . /不需要费心构造一种异常处理结构去UnLock和 /delete /无论是发生了异常还是到了最后,都会调用 /sm. SMLock :SmLock (); / ph.auto_ptr:auto_ptr() ; ,43,对象析构期的异常,在析构函数中抛出异常是极其危险的。它不同于构造函数,析构函数不仅在对象生命期结束时正常被调用,还可能在异常发生时函数栈清退时被调用,此若再发生异常,系统将如何处理呢?只好调用terminate()函数,一走了之。 当果真需要从析构函数中抛出异常时,要首先检查当前是否有未捕获的异常需要处理,若没有,说明属正常销毁对象,于是可以抛出异常;否则千万不要抛出异常。 例如 class DisconnectError ; Clint:Clint( ) throw ( DisconnectError ) if (Disconnect()=failed) if(!uncaught_exception() throw DisconnectError ,对象数组发生异常的处理,若有: class Point3d: public Point2d . ; 在执行 Point3d * pv = new Point3d512; 中,若分配到#21元素时发生了异常,且发生在父类部分已构造完成,但子类部分尚未构造时,则会首先调用Point2d的destructor,(当然不会调用Point3d的destructor ),然后调用20次Point2d的destructor和Point3d的destructor.,全局对象发生异常的处理,由于全局对象是在程序开始运行之前就构造完成,此时若发生异常,将永远不会被捕获。 全局对象的析构也一样,是在程序结束后才被调用,若发生异常,也只能由操作系统才能捕获,应用程序无能为力。,new对异常机制的使用,T * p = new T; 通常会被编译器变成如下样子: T * p = reinterpret_cast (_new charsizeof(T); try new (p) T; catch() deleete p; /释放内存 throw; /二次抛掷 既然是这样,于是建议:将创建对象放在try块中,以便接受二次抛掷 。,分配原始内存,若失败则抛出bad_alloc异常,放置new . 只调用构造函数,不再分配空间,捕获在构造函数调用中发生的异常,二次抛掷的图示,48,一抛,catch,try,main(),try块内函数,try,throw表达式,catch,二抛,镶嵌在catch函数中的不带表达式的throw语句叫二次抛掷.,二次抛掷与重新抛掷的区别,二次抛掷是将原先的异常类型原封不动地再抛出,交给上一层处理结构,并完全退出当前的处理结构。二次抛掷只使用throw,不带任何表达式。 注意:再次抛掷的异常对象是原对象(不是拷贝)。 重新抛掷(rethrow)的区别是改原先的异常类型为另外一个异常类型再抛出。这其实是异常类型转换。,class AllocFailed ; char * GetMemory( size_t size ) throw (AllocFailed) try char * p = new char size ; /可能失败 catch( const bad_alloc ,50,二次抛掷的应用,#include using namespace std; void XFun() try throw “hello“; /抛出char * catch(char *) cout “Caught char * inside XFunn“; throw ; /重新抛出char * void main() try XFun(); catch(char *) cout “Caught char * inside mainn”; ,catch的参数是对象会出问题,借用前面的类class Point3d: public Point2d . ; 若catch子句如下: catch (Point2d p) / do something throw; 在发生异常时,会将Point3d类的异常对象传给p,此时要调用Point2d的拷贝构造函数,切割发生了。若 Point2d 含有虚函数,则p的ptr将指向基类的vtable而非子类的。当二次抛掷发生时,被抛出的是基类的对象已经变形了。避免的手段是使用引用。,( Point2d & p ),class Base public: private: std:string bm1,bm2; ; class Derived: public Base public: Derived() private: std:string dm1,dm2,dm3; ;,53,该函数看上去是空的,果真如此吗?,继承类族中的异常处理,Derived:Derived() /其实该函数应是这个样子: Base :Base(); try dm1.std:string :string(); catch() Base :Base(); throw; try dm2.std:string :string(); catch() dm1.std:string :string(); Base :Base(); throw; try dm3.std:string :string(); catch() dm2.std:string :string(); dm1.std:string :string(); Base :Base(); throw; ,54,课堂练习一: 读代码片断,分析其优劣。 一法: ContainerFault 是父类,StackUnderflow是子类; throw StackUnderflow f; catch( ContainerFault ,55,二法: throw StackUnderflow f; catch( ContainerFault ,56,课堂练习二:读代码,用图示表示调用及抛掷关系。,57,void f() throw 10; void g() . throw 3.2; void h() throw 65.5; try throw a; f(); g(); catch( float ) . throw; ,void k() try h(); catch( float ) . throw 10; catch( int ) . void m() k(); ,void main() try m(); catch( int ) . catch( float ) . catch( . ) . ,异常处理的动态特性,由于异常机制,是在程序运行时有异常抛出时才启动的,编译器无法在编译时检查所抛类型与异常声明列表所列类型是否相符。若抛出的是没有预先声明的类型时,编译器会默认调用unexpected(),该函数的默认行为是调用terminate(),这种逃脱了捕获的现象叫“漏网”。 为了避免发生漏网,可以在程序的开始处,使用库函数set_ubexpected()来预设一个发生这种情况时调用的预留函数回调函数,该函数取代了unexpected(),来完成发生漏网时的处理。,异常接口声明,可以在函数的声明中列出这个函数可能抛掷的所有异常类型。 例如: void fun() throw(A,B,C,D); 若无异常接口声明,则此函数可以抛掷任何类型的异常,也可以不抛 。 void fun(); 不抛掷任何类型异常的函数声明如下: void fun() throw(); 异常接口声明所列异常类型表与实现中所抛异常类型应当一致,不一致时编译器也通过,但运行时则出错。因为编译时无法预知运行时所发生的事情。,为何需要异常接口声明,通常是函数负责抛掷异常,由函数的调用者负责捕获和处理。 清楚地告诉函数的使用者,这个函数可能会抛掷的所有异常的类型,以便于使用者编制与之相呼应的异常处理器(catch) 。 对于无法看到函数实现的函数,外化的只有函数原型,此时函数的调用者必须掌握该函数所能抛掷的异常类型,靠的就是异常接口声明的罗列。,异常接口声明的使用,#include using namespace std; /该函数仅能抛出ints, chars, 和doubles. void XFun(int type) throw(int, char, double) if(type=0) throw type; /抛出int if(type=1) throw a; /抛出char if(type=2) throw 123.23; /抛出double void main() try XFun(0); catch(int i) cout “Caught intn“; catch(char c) cout “Caught charn“; catch(double d) cout “Caught doublen“; ,异常面临的问题,异常机制是仅用于函数的,它与基本类型、基本运算符无关。 可是,运算符重载和模板的出现,使得这个界限模糊了:有的运算符竟也能抛出异常。因为它实质上是函数。,在catch中可以含有return,catch子句中可以含有return 语句,但不属于异常处理的逻辑,而是函数的逻辑为其所在的函数服务的一种功能:当发生异常并被捕获,经处理后,直接返回主调函数去,而不再执行catch之后的语句了。这是将函数的处理逻辑延伸到了catch中。 除非的确需要,不提倡使用这种结构。,异常处理的几种模式,64,catch,main(),函数,try,65,try,main(),try块内函数,catch,66,catch,try,main(),try块内函数,try,catch,67,一抛,catch,try,main(),try块内函数,try,c

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论