2005-4-27 1 C++大学基础教程 第13章 异常处理 北京邮电大学电信工程学院 计算机技术中心 程序设计的要求之一就是程序的健 壮性。希望程序在运行时能够不出 或者少出问题。但是,在程序的实 际运行时,总会有一些因素会导致 程序不能正常运行。异常处理 (Exception Handling)就是要提 出或者是研究一种机制,能够较好 的处理程序不能正常运行的问题。 第十三章 异常处理 13.1 异常和异常处理 13.2 C++异常处理机制 13.3 用类的对象传递异常 13.4 异常处理中的退栈和对象析构 13.1 异常和异常处理 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -5- 13.1.1 异常及其特点 异常(Exceptions)是程序在运行时可能出现 的会导致程序运行终止的错误。 编译系统检查出来的语法错误,导致程序运行 结果不正确的逻辑错误,都不属于异常的范 围。 异常是一个可以正确运行的程序在运行中可能 发生的错误。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -6- 13.1.1 异常及其特点 常见的异常,如: ? 系统资源不足。如内存不足,不可以动态申 请内存空间;磁盘空间不足,不能打开新的 输出文件,等。 ? 用户操作错误导致运算关系不正确。如出现 分母为0,数学运算溢出,数组越界,参数 类型不能转换,等。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -7- 13.1.1 异常及其特点 异常有以下的一些特点: ? 偶然性。程序运行中,异常并不总是会发生 的。 ? 可预见性。异常的存在和出现是可以预见 的。 ? 严重性。一旦异常发生,程序可能终止,或 者运行的结果不可预知。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -8- 13.1.2 异常处理方法及举例 对于程序中的异常,通常有三种处理的 方法: ? 不作处理。很多程序实际上就是不处理异常 的。 ? 发布相应的错误信息,然后,终止程序的运 行。在C语言的程序中,往往就是这样处理 的。 ? 适当的处理异常,一般应该使程序可以继续 运行。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -9- 13.1.2 异常处理方法及举例 一般来说,异常处理(Exception Handling) 就是在程序运行时对异常进行检测和控制。 而在C++中,异常处理(EH)就是用C++提供的 try-throw-catch的模式进行异常处理的机 制。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -10- 例13.1 程序将连续地输入两个实数 ,通过调用函数,返回这 两个数相除的商。并且要注意除数不能为0。 //例13.1 用一般的方法处理除法溢出 #include <iostream.h> #include <stdlib.h> double divide(double a, double b) { if (b == 0) //检测分母是不是 为0 { cout << "除数不可以等于0 !"<<endl; abort(); //调用abort函数终止运行 } return a/b; } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -11- void main() {double x,y,z; cout<<"输入两个实数 x 和 y :"; while (cin >> x >> y) { z = divide(x,y); cout << "x 除以 y 等于 " << z << "\n"; cout << "输入下一组数 <q 表示结束>: "; } cout << "Bye!\n"; } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -12- 如果出现分母为0 的情况,运行将出现以下结果: 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -13- 13.1.2 异常处理方法及举例 这个程序中,对于除数为0的处理有这样 的特点: ? 异常的检测和处理都是在一个程序模块 (divide函数)中进行的; ? 由于函数的返回值是double型的数据,因 此,即使检测到除数为0的情况,也不能通 过返回值来反映这个异常。只能调用函数 abort终止程序的运行。 13.2 C++异常处理机制 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -15- 13.2 C++异常处理机制 C++处理异常有两个基本的做法: ? 异常的检测和处理是在不同的代码段中进行的。一 般的说法是在 “try”部分检测异常, “catch”部分处 理异常。 ? 由于异常的检测和处理不是在同一个代码段中进行 的,在检测异常和处理异常的代码段之间需要有一 种传递异常信息的机制,在C++中是通过 “对象 ”来 传递异常的。这种对象可以是一种简单的数据(如 整数),也可以是系统定义或用户自定义的类的对 象。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -16- 13.2 C++异常处理机制 C++异常处理的语法可以表述如下: try { 受保护语句 ; throw 异常 ; 其他语句 ; } catch(异常类型 ) {异常处理语句 ; } 检测和抛掷 异常 扑获和处理 异常 try模块 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -17- 13.2 C++异常处理机制 在C++术语中,异常(Exception,注意结尾没 有s)是作为专用名词出现的。就是将异常检 测程序所抛掷的 “带有异常信息的对象 ”称为 “异常 ”。 而将捕获异常的处理程序称为异常处理程序 (Exception Handler)。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -18- 13.2 C++异常处理机制 在try复合语句中,可以调用其他函数,在所 调用的函数中检测和抛掷异常,而不是在try 复合语句中直接抛掷异常。这个所调用的函 数,仍然是属于这个try模块的,所以这个模 块中的catch部分,仍然可以捕获它所抛掷的 异常并进行处理。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -19- 例13.2 用C++的异常处理机制,重新处理例13.1。 //例13.2用C++的异常处理机制,处理除法溢出 #include <iostream.h> #include <stdlib.h> double divide(double a, double b) { if (b == 0) { throw "输入错误:除数不可以等于0 !"; } return a/b; } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -20- void main() {double x,y,z; cout<<"输入两个实数 x 和 y :"; while (cin >> x >> y) {try { z = divide(x,y); } catch (const char * s) // start of exception handler { cout << s << "\n"; cout << "输入一对新的实数: "; continue; } // end of handler cout << "x 除以 y 等于 " << z << "\n"; cout << "输入下一组数 <q 表示结束>: "; } cout << "程序结束,再见!\n"; } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -21- 13.2 C++异常处理机制 程序运行的一种结果是: 输入两个实数 x 和 y :1.2 3.2 x 除以 y 等于 0.375 输入下一组数 <q 表示结束>: 3.4 0 输入错误:除数不可以等于0 ! 输入一对新的实数: 2.3 4.5 x 除以 y 等于 0.511111 输入下一组数 <q 表示结束>: q 程序结束,再见! 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -22- 13.2 C++异常处理机制 阅读这个程序,可以注意以下几点: ? 在try的复合语句中,调用了函数divide。因此, 尽管divide函数是在try模块的外面定义的,它仍 然是属于try模块:在try语句块中运行; ? divide函数检测到异常后,抛掷出一个字符串作为 异常对象,异常的类型就是字符串类型; ? catch程序块指定的异常对象类型是char*,可以捕 获字符串异常。捕获异常后的处理方式是通过 continue语句,跳过本次循环,也不输出结果,直 接进入下一次循环,要求用户再输入一对实数。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -23- 13.2 C++异常处理机制 例 13.2的执行过程可以简要的表示如下: 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -24- 13.2 C++异常处理机制 另外,在编写带有异常处理的程序时,还要注 意: ? try语句块和 catch语句块是一个整体, 两者之 间不能有其他的语句 ; ? 一个 try语句块后面可以有多个 catch语句,但 是,不可以几个 try语句块后面用一个 catch语 句。 13.3 用类的对象传递异常 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -26- 13.3 用类的对象传递异常 throw语句所传递的异常,可以是各种类型 的:整型、实型、字符型、指针,等等。也可 以用类对象来传递异常。 对象就是既有数据属性,也有行为属性。使用 对象来传递异常,就是既可以传递和异常有关 的数据属性,也可以传递和处理异常有关的行 为或者方法。 专门用来传递异常的类称为异常类。异常类可 以是用户自定义的,也可以是系统提供的 exception类。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -27- 13.3.1用户自定义类的对象传递异常 我们用第十章中的栈类模板来作为例子,类模 板中两个主要的函数push和pop的定义中,都 安排了错误检查的语句,以检查栈空或者栈满 的错误。由于pop函数是有返回值的,在栈空 的条件下,是没有数据可以出栈的。尽管pop 函数可以检测到这种错误,但是,也不可能正 常的返回,于是只好通过exit函数调用结束程 序的执行。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -28- 13.3.1用户自定义类的对象传递异常 现在,我们用C++异常处理的机制,改写这个 程序。要求改写后的程序不仅有更好的可读 性,而且在栈空不能出栈时,程序也可以继续 运行,使得程序有更好的健壮性。 可以定义两个异常类:一个是 “栈空异常 ”类, 另一个是 “栈满异常 ”类。在try块中,如果检 测到 “栈空异常 ”,就throw一个 “StackEmptyException”类的对象。如果检测 到 “栈满异常 ”,就throw一个 “StackOverflowException”类的对象。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -29- 13.3.1用户自定义类的对象传递异常 例13.3 通过对象传递异常。用C++异常处理机 制来处理栈操作中的 “栈空异常 ”和 “栈满异 常 ”。定义两个相应的异常类。通过异常类对 象来传递检测到的异常,并且对异常进行处 理。要求在栈空的时候用pop函数出栈失败 时,程序的运行也不终止。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -30- //例13.3:带有异常处理的栈 #include <iostream> using namespace std; class StackOverflowException //栈满异常类 {public: StackOverflowException() {} ~StackOverflowException() {} void getMessage() { cout << "异常:栈满不能入栈。" << endl; } }; class StackEmptyException //栈空异常类 {public: StackEmptyException() {} ~StackEmptyException() {} void getMessage() { cout << "异常:栈空不能出栈。" << endl; } }; 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -31- template <class T, int i> // 类 模板定义 class MyStack { T StackBuffer[i]; int size; int top; public: MyStack( void ) : size( i ) {top = i;}; void push( const T item ); T pop( void ); }; 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -32- template <class T, int i> //push成员函数定 义 void MyStack< T, i >::push( const T item ) { if( top >0 ) StackBuffer[--top] = item; else throw StackOverflowException(); //抛掷对象 异常 return; } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -33- template <class T, int i> //pop成员函数定 义 T MyStack< T, i >::pop( void ) { if( top < i ) return StackBuffer[top++]; else throw StackEmptyException(); //抛掷另一个对象异常 } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -34- void main() //带有异常处理的类模板测试 程序 {MyStack<int,5> ss; for(int i=0;i<10;i++) {try {if(i%3)cout<<ss.pop()<<endl; else ss.push(i); } catch (StackOverflowException &e) { e.getMessage(); } catch (StackEmptyException &e) { e.getMessage(); } } cout<<"Bye\n"; } 程序执行的结果是: 0 异常:栈空不能出栈。 3 异常:栈空不能出栈。 6 异常:栈空不能出栈。 Bye 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -35- 13.3.1用户自定义类的对象传递异常 这个例子和例 13.2有一些明显不同的地方: ? 通过对象传递参数。具体来说,是在 throw语句 中直接调用异常类的构造函数,生成一个无名 对象(如: throw StackEmptyException();),来传递异常的。 ? 在 catch语句中规定的异常类型则是异常类对象 的引用。当然,也可以直接用异常类对象作为 异常。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -36- 13.3.1用户自定义类的对象传递异常 ? 通过异常类对象的引用,直接调用异常类的成员函 数 getMessage,来处理异常。 ? 在 try语句块后面直接有两个 catch语句来捕获异 常。也就是说,要处理的异常增加时, catch语句 的数目也要增加。 ? 运行结果表明, 10次循环都已经完成。没有出现因 为空栈时不能出栈而退出运行的情况。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -37- 13.3.2 用 exception类对象传递异常 C++提供了一个专门用于传递异常的类: exception 类。可以通过 exception类的对象来传递异常。 class exception { public: exception(); //默认构造函数 exception(char *); //字符串作参数的构造函数 exception(const exception&); exception& operator= (const exception&); virtual ~exception(); //虚析构函数 virtual char * what() const;//what()虚函数 private: char * m_what; }; 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -38- 13.3.2 用 exception类对象传递异常 其中和传递异常最直接有关的函数有两个: ? 带参数的构造函数。参数是字符串,一般就是 检测到异常后要显示的异常信息。 ? what()函数。返回值就是构造exception类对象 时所输入的字符串。可以直接用插入运算符 “<<”在显示器上显示。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -39- 13.3.2 用 exception类对象传递异常 如果捕获到exception类对象后,只要显示 关于异常的信息,则可以直接使用 exception类。如果除了错误信息外,还需 要显示其他信息,或者作其他的操作,则可 以定义一个exception类的派生类,在派生 类中可以定义虚函数what的重载函数,以便 增加新的信息的显示。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -40- 13.3.2 用 exception类对象传递异常 例13.4 定义一个简单的数组类。在数组类 中重载 “[ ]”运算符,目的是对于数组元素 的下标进行检测。如果发现数组元素下标越 界,就抛掷一个对象来传递异常。并且要求 处理异常时可以显示越界的下标值。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -41- 13.3.2 用 exception类对象传递异常 我们使用 exception类的对 象来传递对象。但是, 直接使用 exception类对象 还是不能满足例题的要 求。因为不能传递越界的下标值。 为此,可以定义一个ex ception 类的派生类 ArrayOverflow。其中包含一个数据成员k。在构 造ArrayOverflow 类对象时,用越界的下标值初始 化这个数据k。在catch块中捕获到这个对象后, 可以设法显示对象的k值。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -42- //例13.4 用exception类参与处理异常 #include <iostream> #include <exception> using namespace std; class ArrayOverflow : public exception //exception 类 的 派生类 {public: ArrayOverflow::ArrayOverflow(int i) : exception( "数组越界异常!\n" ) {k=i;} const char * what() //重新定义的what()函数 {cout<<"数组下标"<<k<<"越界\n"; return exception::what(); } private: int k; }; // 派 生 类 ArrayOverfow定义结束 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -43- class MyArray //数组类的定义 { int *p; //数组首地址 int sz; //数组大小 public: MyArray(int s) { p=new int [s]; sz=s; } //构 造函数 ~MyArray( ) { delete [ ] p ; } int size( ) { return sz; } int& operator[ ] (int i); //重载[]运算符的 原型 }; int& MyArray:: operator[ ] (int i) //重载[]运 算符 {if(i>=0 && i<sz) return p[i]; throw ArrayOverflow( i ); } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -44- void f(MyArray &v); void main() {MyArray A(10); f(A); } void f( MyArray& v ) {//…… for(int i=0;i<3;i++) {try { if(i!=1) {v[i]=i; cout<<v[i]<<endl;} else v[v.size( )+10]=10; } catch( ArrayOverflow &r ) {cout<<r.what(); } } //for循环结束 } 程序运行后输出: 0 数组下标20越界 数组越界异常! 2 13.4 异常处理中的退栈 和对象析构 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -46- 13.4异常处理中的退栈和对象析构 在函数调用时,函数中定义的自动变量 将在堆栈中存放。结束函数调用时,这 些自动变量就会从堆栈中弹出,不再占 用堆栈的空间,这个过程有时被称为 “退 栈 ”(Stack unwinding)。其他的结束 动作还包括调用析构函数,释放函数中 定义的对象。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -47- 13.4异常处理中的退栈和对象析构 但是,如果函数执行时出现异常,并且 只是采用简单的显示异常信息,然后退 出(exit)程序的做法,则程序的执行 就会突然中断,结束函数调用时必须完 成的退栈和对象释放的操作也不会进 行。这样的结果是很不希望的。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -48- float function3(int k) //function3中可能有异常 {if(k==0) {cout<<"function3中发生异常\n"; //显示异常信息 exit(1);} //退出执行 else return 123/k; } void function2(int n) {ForTest A12; function3(n); //第三次调用 } void function1(int m) {ForTest A11; function2(m); //第二次调用 } void main() { function1(0); //第一次调用 } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -49- 13.4异常处理中的退栈和对象析构 程序运行后显示: function3中发生异常 在function1和fuction2中分别定义了ForTest 类的对象。如果函数可以正常退出,这些对象 将被释放。 但是,程序运行后只显示了异常信息。没有析 构函数被调用的迹象。说明所创建的对象没有 被释放。 如果采用C++的异常处理机制来进行处理。情 况就会完全不同。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -50- 13.4异常处理中的退栈和对象析构 如果在 function3中用 throw语句来抛掷 异常,就会开始 function3的退栈。 然后,返回到函数 function2开始 function2的退栈,,并且调用 ForTest 类析构函数,释放对象 A12。 接着,返回到函数 function1开始 function1的退栈,,并且调用 ForTest 类析构函数,释放对象 A11。 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -51- 例13.6 用C++异常处理机制来重新编写例13.5。 // 例 13.6: 用C++异常处理机制,对象可以完全释放 // Demonstrating stack unwinding. #include <iostream> #include <stdlib.h> #include <exception> using namespace std; class ForTest {public: ~ForTest() //析构函数 {cout<<"ForTest类析构函数被调用\n"; } }; //ForTest类定义结束 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -52- float function3(int k) //function3中可能有 异常 {if(k==0) throw exception( "function3中出 现异常\n" ); //抛掷 异常类对象 else return 123/k; } void function2(int n) {ForTest A12; function3(n); //第三次调用 } void function1(int m) {ForTest A11; function2(m); //第二次调用 } 2005-4-27 北京邮电大学电信工程学院计算机技术中心 -53- void main() {try { function1(0); // 第 一次调用 } catch(exception &error) {cout<<error.what()<<endl; } } 程序运行结果显示: ForTest类析构函数被调用 ForTest类析构函数被调用 function3中出现异常 总结 本章介绍了 C++异常处理的机制。在程序设计 中使用这样的异常 处理机制,有助于提高程序 的健壮性、可读性 。而且可以防止因为程序不 正常结束而导致的 资源泄漏,如创建的对象不 能释放等。 本章没有介绍多种 异常处理的结构,而是希望 读者能够抓住最基本的结构: try模块。 另外一个重要的内 容就是通过用户自定义类的 对象来传递异常。 总结 模板分为函数模板和类模板,定义的方 式基本是相同的。在使用上稍有差别: 函数模板通过函数参数的虚实结合就能 得到具体的模板函数。 而使用类模板时,要在类模板名后面具 体说明模板类的实际数据类型。