程序异常处理.docx
- 文档编号:11058746
- 上传时间:2023-02-24
- 格式:DOCX
- 页数:30
- 大小:49.82KB
程序异常处理.docx
《程序异常处理.docx》由会员分享,可在线阅读,更多相关《程序异常处理.docx(30页珍藏版)》请在冰豆网上搜索。
程序异常处理
C++之异常处理
一个好的程序应该能对多种不同的特殊情况,做出不同的反应,对于突发情况也应有对应的处理方法。
我们在编程时应考虑到各种突发情况,并在程序中给出解决方案,使程序的健壮性增强。
假设有一个司机从A地开车前往B地。
若在某处有一岔路口,一般选择左边,路程会近一些。
但当司机选择左边,将车开到途中时发现正在修路(突发情况),无法通过。
这时,司机就会掉头回到刚才的岔路口处,重新选择右边的路,继续前进。
我们所编的程序也应该像这样,有一定的智能化的设计。
这就要求在编写程序时,应该试着确定程序可能出现的错运,然后加入处理错误的代码。
例如:
当程序执行文件I/0操作时,应测试文件打开以及读写操作是否成功,并且在出现错误时做出正确的反应。
随着程序复杂性的增加,为处理错误而必须包括在程序中代码的复杂性也相应地增加了。
为使程序更易于测试和处理错误,C++实现了异常处理机制。
一、 异常概念
1.异常的概念
程序的错误,一种是编译错误,即语法错误。
如果使用了错误的语法、函数、结构和类,程序就无法被生成运行代码。
另一种是在运行时发生的错误,它分为不可预料的逻辑错误和可以预料的运行异常。
运行异常,可以预料,但不能避免,它是由系统运行环境造成的。
如,内存空间不足,而程序运行中提出内存分配申请时,得不到满足,就会发生异常:
#include
//……
voidf(char*str)
{
ifstreamsource(str);//打开str串中的文件
if(source.fail()) //打不开
{cerr<<"Erroropeningthefile:
"< exit (1);//退出程序 } //…… } 当程序对文件打不开时,程序会打印提示信息,并由exit (1)函数退出。 这样就不至于会因为文件打不开而导致整个程序在运行过程中停滞或错乱。 2.异常的基本思想 在小型程序中,一旦发生异常,一般是将程序立即中断运行,从而无条件释放所有资源。 对于大型程序来说,运行中一旦发生异常,应该允许恢复和继续运行。 恢复的过程就是把产生异常所造成的恶劣影响去掉,中间可能要涉及一系列的函数调用链的退栈,对象的析构,资源的释放等。 继续运行就是异常处理之后,在紧接着异常处理的代码区域中继续运行。 在C++中,异常是指从发生问题的代码区域传递到处理问题的代码区域的一个对象。 见图 : 发生异常的地方在函数k()中,处理异常的地方在其上层函数f()中,处理异常后,函数k()和g()都退栈,然后程序在函数f()中继续运行。 如果不用异常处理机制,在程序中单纯地嵌入错误处理语句,要实现这一目的是艰难的。 异常的基本思想是: (1)实际的资源分配(如内存申请或文件打开)通常在程序的低层进行,如图中的k()。 (2)当操作失败、无法分配内存或无法打开一个文件时.在逻辑上如何进行处理通常是在程序的高层,如图中的f(),中间还可能有与用户的对话。 (3)异常为从分配资源的代码转向处理错误状态的代码提供了一种表达方式。 如果还存在中间层次的函数,如图中的g(),则为它们释放所分配的内存提供了机会,但这并不包括用于传递错误状态信息的代码。 从中可以看出,C什异常处理的目的,是在异常发生时,尽可能地减小破坏,周密地善后,而不去影响其它部分程序的运行。 -这在大型程序中是非常必要的。 例如对于以前所讲的程序调用关系,如处理文件打开失败异常的方法,那么,异常只能在发生的函数k()中进行处理,无法直接传递到函数f()中,而且调用链中的函数g()的善后处理也十分困难。 二、异常的实现 使用异常的步骤是: (1)定义异常(try语句块)将那些可能产生错误的语句框定在try语句中; (2)定义异常处理(catch语句块) 将异常处理的语句放在catch块中,以便异常被传递过来时就处理它; (3)抛掷异常(throw语句) 检测是否产生异常,若产生异常,则抛掷异常。 例如,下面的程序,设置了防备文件打不开的异常: 例题1 #include #include #include voidmain(intargc,char**argv) {ifstreamsource(argv[1]);//打开文件 charline[128]; try{ if(source.fail())//如果打开失败 throwargv[1]; } catch(char*s) { cout<<"erroropeningthefile"< exit (1); } while(! source.eof()) {source.getline(line,sizeof(line)); cout< } source.close(); } 运行结果: 假定C盘中没有abc.txt文件,有xyz.txt文件, 内容为: Howareyou? Fine! 两行语句, 则运行结果为: 在c: \>提示符后输入命令ch10_1abc.txt 屏幕显示结果为: erroropeningthefileabc.txt 若输入命令ch10_1xyz.txt则屏幕显示结果为: Howareyou? Fine! 例题2: 一个除零异常: #include doubleDiv(double,double); voidmain() { try {cout<<"7.3/2.0="< } catch(double) { cout<<"exceptofdevidingzero! \n"; } cout<<"Thatisok.\n"; } doubleDiv(doublea,doubleb) {if(b==0.0)throwb;returna/b;} 运行结果为: 7.3/2.0=3.65 exceptofdevidingzero! Thatisok. 三、异常处理机制 在处理程序和语句之间的相互作用使异常在大型应用程序中变得复杂。 通常人们希望抛掷被及时捕获,以避免程序突然终止。 此外,跟踪抛掷很重要,因为捕获确定该程序的后继进展。 例如,抛掷和捕获可以用来重新开始程序内的一个过程,或者从应用程序的一部分跳到另一部分,或者回到菜单。 例如,项目的代码说明了异常处理机制。 voidf() { try{g();} catch(Range) {//……} catch(Size) {//……} catch(…) } voidg() { h(); } voidh() { try {h1(); } catch(Size) {//... throw10; } catch(Matherr) {//... } } voidh1() { //... throw(Size); try {//... throwRange; h2(); h3(); } catch(Size) { //... throw; } } voidh2() { //... throwMatherr; } voidh3() { //... throwSize; } 函数f()中的catch(...)块,参数为省略号,定义一个"默认"的异常处理程序。 通常这个处理程序应在所有异常处理块的最后,因为它与任何throw都匹配,目的是为避免定义的异常处理程序没能捕获抛掷的异常而使程序运行终止。 函数h()中的catch(Size)块,包含有一个抛掷异常语句throw10,当实际执行这条语句时,将沿着调用链向上传递被函数f()中的catch(...)所捕获。 如果没有f()中的catch(...),那么,异常将被系统的terminate()函数调用,后者按常规再调用abort()。 函数h1()中的抛掷throwSize,由于不在本函数的try块中,所以只能沿函 数调用链向上传递,结果被h()中的catch(Size)捕获。 函数h1()中的抛掷throwRange,在try块中,所以首先匹配try块后的异常处理程序,可是没有被捕获,因而它又沿函数调用链向上,在函数f()中,catch(Range)块终于捕获了该抛掷。 函数h1()中的catch(Size)块,包含一个抛掷throw,没有带参数类型,它表示将捕获到的异常对象重新抛掷出去,于是,它将沿函数调用链向上传递,在h()中的catch(Size)块,捕获了该抛掷。 函数h2()中的抛掷throwMatherr,首先传递给h1()中的catch块组,但未能被捕获,然后继续沿调用链向上,在h()中的catch(Matherr)块,捕获了该抛掷。 函数h3()中的抛掷throwSize,向上传递给h1()中的catch块组,被catch(Size)块捕获。 四、使用异常的方法 可以把多个异常组成族系。 构成异常族系的一些实力又数学错误异常族系和文件处理错误异常族系。 在C++代码中把异常组在一起有两种方式: 异常枚举族系和异常派生层次结构。 例如,下面的代码是一个异常枚举族系的例子: enmuFileErrors{nonExist,wrongformat,diakSeekError,...}; intf() { try{//...throwwrongFormat;} catch(FileErrorsfe) {switch(fe) {casenonExist: //... casewrongFormat: //... casediskSeekError: //... } } //... } 在try块中有一个throw,它抛掷一个FileError枚举中的常量。 这个抛掷可被catch(FileErrors)块捕获到,接着后者执行一个switch,对照情况列表,匹配捕获到的枚举常量值。 上面的异常族系也可按异常派生层次结构来实现,如下例 所示: classFileErrors{}; classNonExist: publicFileErrors{}; classWrongFormat: publicFileErrors{}; classDiskSeekError: publicFileErrors{}; intf() {try{//...throwWrongFormat;} catch(NonExist) {//...} catch(DiskSeekError) {//...} catch(FileErrors) {//...} //... } 上面的各异常处理程序块定义了针对类NonExist和DiskSeekError的派生异常类对象,针对FileErrors的异常处理,既捕获FileErrors类对象,也捕获WrongFormat对象。 异常捕获的规则除了前面所说的,必须严格匹配数据类型外,对于类的派生,下列情况可以捕获异常: (1)异常处理的数据类型是公有基类,抛掷异常的数据类型是派生类; (2)异常处理的数据类型是指向公有基类的指针,抛掷异常的数据类型是指向派生类的指针。 对于派生层次结构的异常处理,catch块组中的顺序是重要的。 因为"catch(基类)"总能够捕获"throw派生类对象"。 所以"catch(基类)"块总是放在"catch(派生类)"块的后面,以避免"catch(派生类)"永远不能捕获异常。 五、异常的再提出(rethrowing) 有时候会发生一个异常处理句柄接收了一个异常却发现不能处理这个异常的情况。 这时,这个异常可以被再提出以便于其它句柄能够更好的处理。 异常的再提出可以通过一个空的throw表达语句来实现。 但是,这种表达语句只能出现于一个异常处理句柄中。 例如, voidf() { try { showWindow();//提出CoDialogException类异常 } catch(CoWindowException&WinExc) { WinExc.repaint(); throw;//异常再提出 } } voidg() { try { f(); } catch(CoDialogException&DialogExc) {/*异常处理语句*/} catch(CoWindowException&WindowExc) {/*异常处理语句*/} } 上述例子中,尽管CoDialogException类异常是由函数f()中的CoWindowException类处理句柄再提出的,但是它仍然由函数g()中的CoDialogException类异常处理句柄来处理。 此外,任何异常都可以通过一种特殊的接收方式catch(...)来接收和处理。 例如下面例子中的f()函数可以接收任何异常并再提出。 voidf() { try { showWindow(); } catch(...)//接收任何异常 { //某些处理语句 throw; } } 值得注意的是,异常的再提出并不对异常对象进行进一步的拷贝。 #include #include using namespace std; enum {SUCCESS, FAILURE}; class File { public: File (const char *) {} public: bool IsValid() const {return false; } public: int OpenNew() const {return FAILURE; } }; class Exception {/*..*/}; //general base class for exceptions class FileException: public Exception { public: FileException(const char *p) : s(p) {} public: const char * Error() const { return s.c_str(); } private: string s; }; void func(File& ); int main() { try //outer try { File f ("db.dat"); func(f); // 1 } catch(...) // 7 //this handler will catch the re-thrown exception; //note: the same exception type is required { cout<<"re-thrown exception caught"; } return 0; } void func(File & f) { try //inner try { if (f.IsValid() == false ) throw FileException("db.dat"); // 2 } catch(FileException &fe) // 3 //first chance to cope with the exception { cout<<"invalid file specification" < if (f.OpenNew() ! = SUCCESS) (5) //re-throw the original exception and let a higher handler deal with it throw; // 6 } } 在上面的例子中,函数func()在main()中的try block里被调用 (1)。 第二个在func()中的try block抛出一个FileException类型的异常 (2)。 这个异常被func()内的catch block所捕获(3)。 那个catch block试图通过打开一个新文件进行补救,但是失败了(5),并且FileException异常被重新抛出(6)。 最终,那个重新抛出的异常被main()中的catch(…)所捕获(7)。 六.在异常对象中携带更多的信息 异常对象如同其它类对象一样可以携带信息。 所以一个异常对象可以被用来将一些有用信息从提出点携带到接收处理点。 这些信息可以是当程序在运行过程中出现非正常情况时程序使用者想要知道的。 例如一个程序使用者可能想知道一个矢量的下标,当这个矢量下标超限时。 classCoVector { public: CoVector(int); classRange; int&operator[](inti); protected: int*pInt_; inttheSize; }; classCoVector: : Range { public: CoVector: : Range(int); intindex_; }; CoVector: : Range: : Range(inti): index_(i) {/*...*/} int&CoVector: : operator[](inti) { if(0<=i&&i< returnpInt_[i]; throwRange(i); } voidf(constCoVector&v) { try { inttemp=v[169]; } catch(constCoVector: : Range&r) { cout<<"badindex="< } } 实际上,catch后面括弧中的表达语句实际上类似于函数的参数定义。 七、句柄的次序 异常处理句柄有先后次序之分。 因为一个派生(derived)异常类对象可以被几个句柄接收,所以在排列异常处理句柄顺序时应该特别小心。 另外,一个类型严格匹配的处理句柄并不比一个需要类型转换的处理句柄更有优先权。 例如下面的例子就很糟糕。 classCoWindow {/*...*/}; classCoButton: publicCoWindow {/*...*/}; voidf(intv) { typedefvoid(*PCF)(constchar*); try { if(v)throw&v; //其它表达语句 } catch(void*pVoid){/*...*/} catch(PCFpFunction){/*...*/} catch(constCoWindow&win){/*...*/} catch(constCoButton&button){/*...*/} catch(...){/*...*/} return; } 在上面例子中,(void*)处理句柄不可能允许它后面的PCF处理句柄被调用。 类似的,因为CoWindow类处理句柄将会接收任何CoWindow类及它的衍生类对象,所以CoButton类的处理句柄也不会被调用。 依赖于你所使用的编译器,有的编译器可能在编译时警告你一个从类B派生来的类D句柄放在类B句柄后面。 但是,如果一个接收任何异常的句柄catch(...)不是最后一个句柄,编译器会给出编译错误。 派生类组织异常; ClassMatherr{}; ClassOverflow: publicMatherr{}; ClassUnderflow: publicMatherr{}; Class: Zerodivide: publicMatherr{}; … Try{ } Cath(Overflow) { } Cath(Uunderflow) { } Cath(Zerodivideflow) { } Cath(Matherr) { } 八、异常提出过程中的对象构造和析构 当一个程序由于异常而中断时,所有的从try开始构造的自动变量类的对象都会被清除、释放。 这种调用自动变量类的析构函数的过程称为堆栈清除(stackunwinding)。 下面给出了一个实例。 classCoClass { public: intv_; CoClass(intv=0): v_(v) { cout<<"CoClass(int): "< } ~CoClass() { cout<<"~CoClass(): "< } }; classCoError { public: intv_; CoError(intv=0): v_(v) { cout<<"CoError(int): "< } CoError(constCoError&ve): v_(ve.v_) { cout<<"CoError(constCoError&): "< } ~CoError() { cout<<"~CoError(): "< } }; intf(intv) { if(v==13) { CoClassvc(0); throwCoError(v); } returnv; } intmain() { try { CoClassvc(169); f(13); } catch(constCoError&e) { cout<<"Caught: "< } return0; } 这个例子给出了下面输出结果。 当catch(constCoErrore)时 CoClass(int): 169 如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。