1.3 C++异常处理
程序中的错误通常包括:语法错误、逻辑错误和运行时异常(Exception)。其中,语法错误通常是指函数、类型、语句、表达式、运算符或标识符的使用不符合C++中的语法,这种错误在程序编译或连接时就会由编译器指出;逻辑错误是指程序能顺利运行,但是没有实现或达到预期的功能或结果,这类错误常需要通过调试或测试才能发现;运行时异常是指在程序运行过程中,由于意外事件的发生而导致程序异常终止,如内存空间不足、打开的文件不存在、零除数、下标越界等。
异常或错误的处理方法有很多,如判断函数返回值、使用全局的标志变量、直接使用C++中的exit()或abort()函数来中断程序的执行。
1.3.1 使用C++异常处理
程序运行时异常的产生虽然无法避免,但可以预料。为了保证程序的健壮性,必须在程序中对运行时异常进行预见性处理,这种处理称为异常处理。
C++提供了专门用于异常处理的一种结构化形式的描述机制try/throw/catch。该异常处理机制能够把程序的正常处理和异常处理逻辑分开表示,使得程序的异常处理结构清晰,通过异常集中处理的方式,解决异常处理的问题。
1.try语句块
try语句块的作用是启动异常处理机制,侦测try语句块中的程序语句执行时可能产生的异常。如有异常产生,则抛出异常。try的格式如下:
注意:try总是与catch一同出现,在一个try语句块之后,至少应该有一个catch语句块。
2.catch语句块
catch语句块用来捕捉try语句块产生的异常或用throw抛出的异常,然后进行处理,其格式如下:
其中,catch中的形参类型可以是C++基本类型(如int、long、char等)、构造类型,还可以是一个已定义的类的类型,包括类的指针或者引用类型等。如果在catch中指定了形参名,则可像一个函数的参数传递那样将异常值传入,并可在catch语句块中使用该形参名。例如:
try { throw "除数不能为0!"; } catch(const char * s) // 指定异常形参名 { cout<<s<<endl; // 使用异常形参名 }
注意:
(1) 当catch中的整个形参为“…”时,则表示catch能捕捉任何类型的异常。
(2) catch前面必须是try语句块或另一个catch块。正因如此,在书写代码时应使用这样的格式:
try { … } catch(…) { … } catch(…) { … }
3.throw
throw用来强行抛出异常,其格式如下:
其中,异常类型表达式可以是类对象、常量或变量表达式等。
4.三者关系和注意点
throw和catch的关系就好比函数调用关系,catch指定形参,而throw给出实参。编译器将按照catch出现的顺序及catch指定的参数类型确定throw抛出的异常应该由哪个catch来处理。
throw不一定出现在try语句块中,实际上,它可以出现在任何需要的地方,即使在catch中的语句块中,仍然可以继续使用throw,只要最终有catch可以捕获它即可。例如:
class Overflow { // ... public: Overflow(char,double,double); }; void f(double x) { // ... throw Overflow('+',x,3.45e107);// 在函数体中使用throw,用来抛出一个对象 } try { // ... f(1.2); // ... } catch(Overflow& oo) { // 处理Overflow类型的异常 }
当throw出现在catch语句块中时,通过throw既可以重新抛出一个新类型的异常,也可以重新抛出当前这个异常,在这种情况下,throw不应带任何实参。例如:
try { ... } catch(int) { throw "hello exception"; // 抛出一个新的异常,异常类型为const char* } catch(float) { throw; // 重新抛出当前的float类型异常 }
1.3.2 嵌套异常和栈展开
在C++中,异常处理嵌套一般指下列结构:
当然,在程序代码中,若一个函数中的异常处理语句块中还有另一个函数的调用,而另一个函数本身也会产生异常,这样,通过函数嵌套调用也会形成异常处理嵌套。
在嵌套异常情况下,最底层函数所抛出的异常首先在内层中依次查找相匹配的catch语句块,只要遇到第一个匹配的catch子句,查找就会结束,然后进入该catch子句,进行处理。若没有匹配,则内层函数产生的异常逐层向外传递,最后回到主程序中。这种因发生异常而逐步退出复合语句或函数定义的过程,称为栈展开(stack unwinding)过程。
需要说明的是:
(1) 随着栈展开,在退出的复合语句和函数定义中声明的局部变量的生命期也结束了。在栈中分配的局部变量所占用的资源也被释放,由系统回收。但是,如果函数动态分配过内存或其他资源(包括用new运算符取得的资源和打开的文件)出现异常,这些资源的释放语句可能被忽略,从而造成这些资源将永远不会自动释放。
(2) 在栈展开期间,当一个复合语句(或语句块)或函数退出时,若遇到的局部变量是类对象时,则栈展开过程将自动调用该对象的析构函数,完成资源的释放。