第十章 异常处理 大型和十分复杂的程序往往会产生一些很难查找的甚至是无法避免的运行时错误。当发生运行时错误时,不能简单地结束程序运行,而是退回到任务的起点,指出错误,并由用户决定下一步工作。面向对象的异常处理(exception handling)机制是C++语言用以解决这个问题的有力工具。 函数执行时,放在try(测试)程序块中的任何类型的数据对象发生异常,都可被throw块抛出,随即沿调用链退回,直到被catch块捕获,并在此执行异常处理,报告出现的异常等情况。从抛出到捕获,应将各嵌套调用函数残存在栈中的自动对象、自动变量和现场保护内容等进行清除。如果已退到入口函数还未捕获则由abort()来终结入口函数。 异常处理在C++编程中已经普遍采用,成为提高程序健壮性的重要手段之一。
第十章 异常处理 10.1 异常的概念 10.2 异常处理的机制 10.5 异常规范 10.3 捕获异常 10.6 异常和继承 第十章 异常处理 10.1 异常的概念 10.2 异常处理的机制 10.5 异常规范 10.3 捕获异常 10.6 异常和继承 10.4 异常的重新抛出 和catch_all子句 10.7 C++标准库异常类 层次结构
10.1 异常的概念 的,运行时不正常的情况,如存储空间耗尽、数组越 10.1 异常的概念 这里所讲的异常(exception)是程序可能检测到 的,运行时不正常的情况,如存储空间耗尽、数组越 界、被0除等等,可以预见可能发生在什么地方,但是无法确知怎样发生和何时发生。特别在一个大型的程序(软件)中,程序各部分是由不同的小组编写的,它们由公共接口连起来,错误可能就发生在相互的配合上,也可能发生在事先根本想不到的个别的条件组合上。 本章介绍的技术,尽管是为大型软件工程开发所发展的,但是它在标准C++中已经成为一个标准的技术,在任何规模的程序中都可以使用。 C++提供了一些内置的语言特性来产生(raise)或抛出(throw)异常,用以通知“异常已经发生”,然后由预先安排的程序段来捕获(catch)异常,并对它进行处理。这种机制可以在C++程序的两个无关(往往是独立开发)的部分进行“异常”通信。由程序某一部分引发了另一部分的异常,这一异常可回到引起异常的部分去处理(沿着程序函数的调用链)。这也是分清处理责任的好办法。
10.2 异常处理的机制 首先,在C++中异常往往用类(class)来实现,以栈为例,异常类声明如下: 10.2 异常处理的机制 首先,在C++中异常往往用类(class)来实现,以栈为例,异常类声明如下: class popOnEmpty{...}; //栈空异常 class pushOnFull{...}; //栈满异常 不再是一测到栈满或空就退出程序了,而是抛出一个异常。 template <typename T>void Stack<T>::Push(const T&data){ if(IsFull()) throw pushOnFull<T>(data); //注意加了括号,是构造一个无名对象 elements[++top]=data; } template<typename T>T Stack<T>::Pop(){ if(IsEmpty()) throw popOnEmpty<T>(); return elements[top--]; } 注意pushOnFull是类,C++要求抛出的必须是对象,所以必须有“()”,即调用构造函数建立一个对象。异常并非总是类对象,throw表达式也可以抛出任何类型的对象,如枚举、整数等等。但最常用的是类对象。throw表达式抛出异常为异常处理的第一步。在堆栈的压栈和出栈操作中发生错误而抛出的异常,理所当然地应由调用堆栈的程序来处理。
10.2 异常处理的机制 在C++中建立异常抛出与异常处理之间有一整套程序设计的机制。首先采用关键字try,构成一个try块(try block),它包含了抛出异常的语句。当然也可以是包含了这样的调用语句,该语句所调用的函数中有能够抛出异常的语句。 请看下面的程序段给出try块与catch子句的关系: int main(){ int a[9]={1,2,3,4,5,6,7,8,9},b[9]={0},i; stack<int>istack(8); try{ for(i=0;i<9;i++) istack.Push(a[i]); istack.PrintStack(); } catch(pushOnFull<int>){cerr<<”栈满”<<endl;} try{ for(i=0;i<9;i++){b[i]=istack.Pop();} } catch(popOnEmpty<int>){cerr<<”栈空”<<endl;} for(i=0;i<9;i++) cout<<b[i]<<’\t’; cout<<endl; return 0; } 这里有两个try块,分别对应压栈与出栈;也有两个catch子句(catch clause),分别处理压栈时的栈满和出栈时的栈空。
10.2 异常处理的机制 由catch字句捕获并处理异常是第二步。注意与catch语句分别匹配的是在压栈和出栈成员函数模板中的throw语句,一个抛出pushOnFull类的无名对象,另一个抛出popOnEmpty类的无名对象。 程序按下列规则控制: 1.如果没有异常发生,继续执行try块中的代码,与try块相关联 的catch子句被忽略,程序正常执行,main()返回0。 2.当第一个try块在for循环中抛出异常,则该for循环退出,try块也退出,去执行pushOnFull异常的catch子句。istack.PrintStack()不再执行,被忽略。 3.如果第二个try块调用Pop()抛出异常,则退出for和try块,去执行popOnEmpty异常的catch子句。 4.当某条语句抛出异常时,跟在该语句后面的语句将被跳过。程序执行权交给处理异常的catch子句,如果没有catch子句能够处理异常,则交给C++标准库中定义的terminate()。 在编制程序时有一条惯例:把正常执行的程序与异常处理两部分分隔开来,这样使代码更易于跟随和维护。在上例中,我们可以把两个try块合成一个,而把两个catch子句都放在函数最后。
10.2 异常处理的机制 int main() try{ int a[9]={1,2,3,4,5,6,7,8,9},b[9]={0}; 10.2 异常处理的机制 把程序的正常处理代码和异常处理代码分离的最清楚的方法是定义函数try块(function try block), 但VC++6.0不支持。这种方法是把整个函数包括在try块中: int main() try{ int a[9]={1,2,3,4,5,6,7,8,9},b[9]={0}; stack <int>istack(8); ......; return 0; } catch(popOnEmpty<int>){cerr<<”栈空”<<endl;return 1;} catch(pushOnFull<int>){cerr<<”栈满”<<endl;return 2;} 一个函数try块把一组catch子句同一个函数体相关联。如果函数体中的语句抛出一个异常,则考虑跟在函数体后面的处理代码来处理该异常。函数try块对构造函数尤其有用。 catch子句必须在try块之后;而try块后必须紧跟一个或多个catch子句,目的是对发生的异常进行处理。catch的括号中只能有一个类型,当类型与抛掷异常的类型匹配时,称该catch子句捕获了一个异常,并转到该块中进行异常处理。
10.3 捕获异常 catch子句由三部分组成:关键字catch、圆括号中的异常声明以及复合语句中的一组语句。 *注意这不是函数,所以圆括号中不是形参,而是一个异常类型声明,可以是类型也可以是对象。看一看catch子句的使用就可知它与函数的不同了:它只有一个子句,没有定义和调用之分。使用时由系统按规则自动在catch子句列表中匹配。至少从逻辑上讲,没有函数的定义与调用。 catch子句可以包含返回语句(return),也可不包含返回语句。包含返回语句,则整个程序结束。而不包含返回语句,则执行catch列表之后的下一条语句。 异常声明中也可以是一个对象声明。以栈为例。当栈满时,要求在异常对象中保存不能被压入到栈中的值,这时,pushOnFull类可定义如下: template <typename T>class pushOnFull{ T _value; public: pushOnFull(T i):_value(i){} //或写为pushOnFull(T i){_value=i;} T value(){return _value;} }; 新的私有数据成员_value保存那些不能被压入栈中的值。该值即调用构造函数时的实参。
10.3 捕获异常 对应在throw表达式中,构造抛出对象也要有实参: throw pushOnFull(data); 10.3 捕获异常 对应在throw表达式中,构造抛出对象也要有实参: throw pushOnFull(data); //data即Push(const &data)中的参数data 这样在catch子句中,要取得_value,须调用pushOnFull中的成员函数value(): catch(pushOnFull<T> eObj){ cerr<<”栈满”<<eObj.value()<<”未压入栈”<<endl; return 1;} 在catch子句的异常声明中声明了对象eObj,用它来调用pushOnFull类的对象成员函数value()。异常对象是在抛出点被创建,与catch子句是否显式要求创建一个异常对象无关,该对象总是存在,在catch子句中只是为了调用异常处理对象的成员函数才声明为对象,不用类。 *catch子句异常声明中采用对象只是一种形式。甚至异常并非一个类对象时,也可以用同样的格式,比如异常为一枚举量,这时就等效于按值传递,而不是调用类对象的公有成员。
10.3 捕获异常 下面的讨论展现了C++异常处理的不可替代的技术。 10.3 捕获异常 catch子句的异常声明与函数参数声明类似,可以是按值传送,也可以是按引用传递。如果catch子句的异常声明改为引用声明,则catch子句可以直接引用由throw表达式创建 的异常对象,而不必创建自己的局部拷贝。对大型类对象减少不必要的拷贝是很有意义的,所以对于类类型的异常,其异常声明最好也是被声明为引用。如: catch(pushOnFull<T> & eObj){ cerr<<”栈满”<<eObj.value()<<”未压栈”<<endl; return 1; } 使用引用类型的异常声明,catch子句能够修改异常对象,但仅仅是异常对象本身,正常程序部分的量并不会被修改。与一般类对象不同,实际上异常对象处理完后,生命期也就结束了。只有需要重新抛出异常(在下一节中讨论),修改操作才有意义。 下面的讨论展现了C++异常处理的不可替代的技术。
寻找匹配的catch子句有固定的过程:如果throw表达式位于try块中,则检查与try块相关联的catch子句列表,看是否有一个子句能够处理该异常,有匹配的,则该异常被处理;找不到匹配的catch子句,则在主调函数中继续查找。如果一个函数调用在退出时带有一个被抛出的异常未能处理,而且这个调用位于一个try块中,则检查与该try块相关联的catch子句列表,看是否有一个子句匹配,有,则处理该异常;没有,则查找过程在该函数的主调函数中继续进行。即这个查找过程逆着嵌套的函数调用链向上继续,直到找到处理该异常的catch子句。只要遇到第一个匹配的catch子句,就会进入该catch子句,进行处理,查找过程结束。
10.3 捕获异常 因发生异常而逐步退出复合语句和函数定义,被称为栈展开(stack unwinding)。这是异常处理的核心技术。 10.3 捕获异常 因发生异常而逐步退出复合语句和函数定义,被称为栈展开(stack unwinding)。这是异常处理的核心技术。 在栈异常处理的例子中,对popOnEmpty,首先应在istack的成员函数Pop()中找,因为Pop()中的throw表达式没有在try块中,所以Pop()带着一个异常退出。下一步是检查调用Pop()的函数,这里是main(),在main()中对Pop()的调用位于一个try块中,则可用与该try块关联的catch子句列表中的某一个来处理,找到第一个popOnEmpty类型异常声明的catch子句,并进入该子句进行异常处理。 异常对程序的影响通常不仅是在发生异常的那个局部范围中,而且可能逆调用链而上,甚至整个任务。因此,异常处理应该在其对程序影响的终结处进行,甚至是在调用该任务的菜单处进行。
10.3 捕获异常 退出调用链时必须释放所有资源,由系统回收。 10.3 捕获异常 退出调用链时必须释放所有资源,由系统回收。 在栈展开期间,当一个复合语句(或语句块)或函数退出时,在退出的域中有某个局部量是类对象,栈展开过程将自动调用该对象的析构函数,完成资源的释放。所以C++异常处理过程本质上反映的是“资源获取是由构造函数实现,而资源释放是由析构函数完成”这样一种程序设计技术。采用面向对象的程序设计,取得资源的动作封装在类的构造函数中,释放资源的动作封装在类的析构函数中,当一个函数带着未处理的异常退出时,函数中这种类对象被自动销毁,资源(包括动态空间分配的资源和打开的文件)释放。栈展开过程决不会跳过封装在类的析构函数中的资源释放动作。所以由文件重构对象应该放在构造函数中,而把对象存入文件应该放在析构函数中。 异常处理应该用于面向对象的程序设计。对非面向对象的程序设计如果函数动态获得过资源,因异常,这些资源的释放语句可能被忽略,则这些资源将永远不会被自动释放。
10.3 捕获异常 异常对象是在throw表达式中建立并抛出:throw表达式通过调用异常类的构造函数创建一个临时对象,然后把这个临时对象拷贝到一个被称为异常对象(exception object)的存贮区中,它保证会持续到异常被处理完。 异常不能够保持在未被处理的状态。异常表示一个程序不能够继续正常执行,这是非常严重的问题,如果没有找到处理代码,程序就调用C++标准库中定义的函数terminate()。terminate()的缺省行为是调用abort(),指示从程序中非正常退出。 函数调用和异常处理之间的异同:throw表达式的行为有点像函数的调用,而catch子句有点像函数定义。函数调用和异常处理的主要区别是:建立函数调用所需要的全部信息在编译时已经获得,而异常处理机制要求运行时的支持。对于普通函数调用,通过函数重载解析过程,编译器知道在调用点上哪个函数会真正被调用。但对于异常处理,编译器不知道特定的throw表达式的catch子句在哪个函数中,以及在处理异常之后执行权被转移到哪儿。这些都在运行时刻决定,异常是随机发生的,异常的处理的catch子句是沿调用链逆向进行查找,这与运行时的多态——虚函数也是不一样的。当一个异常不存在处理代码时,编译器无法通知用户,所以要有terminate()函数,它是一种运行机制,当没有处理代码(catch子句)能够匹配被抛出的异常时由它通知用户。
10.4 异常的重新抛出和catch_all子句 rethrow表达式仍为: throw; 当catch语句捕获一个异常后,可能不能完全处理异常,完成某些操作后,该异常必须由函数链中更上级的函数来处理,这时catch子句可以重新抛出(rethrow)该异常,把异常传递给函数调用链中更上级的另一个catch子句,由它进行进一步处理。 rethrow表达式仍为: throw; 但仅有一个关键字,因为异常类型在catch语句中已经有了,不必再指明。被重新抛出的异常就是原来的异常对象。但是重新抛出异常的catch子句应该把自己做过的工作告诉下一个处理异常的catch子句,往往要对异常对象做一定修改,以表达某些信息,因此catch子句中的异常声明必须被声明为引用,这样修改才能真正做在异常对象自身中,而不是拷贝中。
10.4 异常的重新抛出和catch_all子句 通常异常发生后按栈展开(stack unwinding)退出,动态分配的非类对象资源是不会自动释放的,应该在对应的catch子句中释放。 因为我们不知道可能被抛出的全部异常,所以不是为每种可能的异常写一个catch子句来释放资源,而是使用通用形式的catch子句catch_all: catch(...){代码*/} 对任何异常都可以进入这个catch子句。这里的三个点称为省略号。花括号中的复合语句用来执行指定操作,当然可以包括资源的释放。 void fun1(){ int *res; res=new int[100]; //定义一个资源对象 try{ //代码包括使用资源res和某些可能引起异常抛出的操作,异常可能有多种 } catch(...){ //不论是那种异常都在此释放 delete [] res; //释放资源对象res throw; //重新抛出异常 delete [] res; //正常退出前释放资源对象res;
10.4 异常的重新抛出和catch_all子句 catch_all子句可以单独使用,也可以与其它catch子句联合使用。如果联合使用,它必须放在相关catch子句表的最后。因为catch子句被检查的顺序与它们在try块之后排列顺序相同,一旦找到了一个匹配,则后续的catch子句将不再检查,按此规则,catch_all子句(catch(...){})处理表前面所列各种异常之外的异常。如果只用catch_all子句进行某项操作,则其他的操作应由catch子句重新抛出异常,沿调用链逆向去查找新的处理子句来处理,而不能在子句列表中再按排一个处理同一异常的子句,因为第二个子句是永远执行不到的。
10.5 异常规范 必须指出VC++6.0不支持异常规范,编程可以包括异常规范,实际什么也未做 。 10.5 异常规范 异常规范(exception specification)提供了一种方案,可以随着函数声明列出该函数可能抛出的异常,并保证该函数不会抛出任何其他类型的异常,在stack类定义中可有: void Push(const T&data) throw(pushOnFull); T Pop() throw(popOnEmpty); 如果成员函数是在类外定义,则类内声明和类外定义必须都有同样的异常规范。 一个函数的异常规范的违例只能在运行时才能被检测出来。如果在运行时,函数抛出了一个没有被列在它的异常规范中的异常时,则系统调用C++标准库中定义的函数unexpected()。 必须进一步指出,仅当函数中所抛出的异常,没有在该函数内部处理,而是沿调用链回溯寻找匹配的catch子句的时候,异常规范才起作用。 在函数指针的声明中也可给出一个异常规范,它所指向的函数也必须有同样的异常规范,或者是其中的一部分(子集)。 如果异常规范为throw(),则表示不得抛出任何异常。 必须指出VC++6.0不支持异常规范,编程可以包括异常规范,实际什么也未做 。
学习了前几节内容后,给出下例作为小结 【例10.1】包含栈满或空异常的较完整的程序段。 ....... template<typename T>class pushOnFull{ T _value; public: pushOnFull(T i){_value=i;} T value(){return _value;} void print(){cerr<<”栈满,”<<value()<<”未压入栈”<<endl;} }; template<typename T>class popOnEmpty{ void print(){cerr<<”栈已空,无法出栈”<<endl;} }; ...... template<typename T>class Stack{ void Push(const T&data) throw(pushOnFull<T>); T Pop() throw(popOnEmpty<T>); ...... }
10.5 异常规范 template<typename T> void Stack<T>::Push(const T &data) throw(pushOnFull<T>){ if(IsFull()) throw pushOnFull<T>(data); //栈满则抛出异常 elements[++top]=data;} template<typename T>T Stack<T>::Pop() throw(popOnEmpty<T>){ if(IsEmpty()) throw popOnEmpty<T>(); //栈已空则不能退栈,抛出异常 return elements[top--]; } //返回栈顶元素,同时栈顶指针退1 ...... int main(){ int a[9]={1,2,3,4,5,6,7,8,9}, b[9]={0},i; Stack<int>istack(8); try{ for(i=0;i<9;i++) istack.Push(a[i]); //到a[8]时栈满,异常 istack.PrintStack(); } catch(pushOnFull<int>&eObj){eObj.print();} try{for(i=0;i<9;i++) b[i]=istack.Pop();} catch(popOnEmpty<int>&eObj){ eObj.print();} for(i=0;i<9;i++) cout<<b[i]<<’\t’; cout<<endl; return 0; }
10.6 异常和继承 在C++程序中,表示异常的类通常被组成为一个组(即如在前面各节讨论的那样)或者一个层次结构。对由栈类成员函数抛出的异常,可以定义一个称为Excp的基类: class Excp{...}; 再从该基类派生出这两个异常类: class popOnEmpty:public Excp{...}; class pushOnFull:public Excp{...}; 由基类Excp来打印错误信息: class Excp{ public:void print(string msg){cerr<<msg<<endl;} }; 这样的基类也可以作为其他异常类的基类: class Excp{...}; //所有异常类的基类 class stackExcp:public Excp{...}; //栈异常类的基类 class popOnEmpty:public stackExcp{...}; //栈空退栈异常 class pushOnFull:public stackExcp{...}; //栈满压栈异常 class mathExcp:public Excp{...}; //数学库异常的基类 class zeroOp:public mathExcp{...}; //数学库零操作异常 class divideByZero:public mathExcp{...}; //数学库被零除异常
10.6 异常和继承 形成了三层结构。在层次结构下,异常的抛出会有一些不同: if(full()){ 10.6 异常和继承 形成了三层结构。在层次结构下,异常的抛出会有一些不同: if(full()){ pushOnFull except(data); StackExcp *pse=&except; //pse指向的类对象为pushOnFull throw *pse; } //抛出的异常对象的类型为stackExcp 这里被创建的异常类对象是stackExcp类类型,尽管pse指向一个实际类型为pushOnFull的对象,但那是一个临时对象,拷贝到异常对象的存储区中时创建的却是stackExcp类的异常对象。所以该异常不能被pushOnFull类型的catch子句处理。这里的规则与第八章的虚函数相反。请对比例10.2。 在处理类类型异常时,catch子句的排列顺序是非常重要的。catch(pushOnFull){...}//处理pushOnFull异常 catch(stackExcp){...}//处理栈的其他异常 catch(Excp){...}//处理一般异常 派生类类型的catch子句必须先出现,以确保只有在没有其他catch子句适用时,才会进入基类类型的catch子句。 异常catch子句不必是与异常最匹配的catch子句,而是最先匹配到的catch子句,就是第一个遇到的可以处理该异常的catch子句。所以在catch子句列表中最特化的(匹配条件最严格的)catch子句必须先出现。
10.6 异常和继承 类层次结构的异常同样可以重新抛出(rethrow),把一个异常传递给函数调用列表中,更上层的另一个catch子句: 10.6 异常和继承 类层次结构的异常同样可以重新抛出(rethrow),把一个异常传递给函数调用列表中,更上层的另一个catch子句: throw; 重新抛出的异常仍是原来的异常对象。如果程序中抛出了pushOnFull类类型的异常,而它被基类的catch子句处理,并在其中再次被抛出,那么这个异常仍是pushOnFull类类型的异常,而不是其基类类型的异常。 在基类catch子句处理的是异常对象的基类子对象的一份拷贝,该拷贝只在该catch子句中被访问,重新抛出的是原来的异常对象。这个放在异常对象存储区中的异常的生命期应该是在处理该异常的一系列的子句中最后一个退出时才结束,也就是直到这时,才由异常类的析构函数来销毁它。这一系列的子句是由重新抛出联系起来的。 虚函数是类层次结构中多态性的基本手段,异常类层次结构中也可以定义虚拟函数。
10.6 异常和继承 【例10.2】异常层次结构中的虚函数。为了调用派生类对象的虚拟函数,异常声明必须为一个指针或引用 10.6 异常和继承 【例10.2】异常层次结构中的虚函数。为了调用派生类对象的虚拟函数,异常声明必须为一个指针或引用 class Excp{public: virtual void print(){cerr<<”发生异常”<<endl;} }; class stackExp:public Excp{ public: virtual void print(){cerr<<”栈发生异常”<<endl;} }; class pushOnFull:public stackExcp{ public: virtual void print(){cerr<<”栈满,不能压栈”<<endl;} }; class popOnEmpty:public stackExcp{ public: void print(){cerr<<"栈已空,无法出栈"<<endl;} }; int main(){ try{ //抛出一个pushOnFulll异常 } catch(Excp&eObj){ eObj.print(); } //调用虚函数pushOnFull::print() try{ //抛出一个popOnEmpty 异常 catch子句输出为: 栈满,不能压栈 栈已空,无法出栈
10.6 异常和继承 对异常规范(exception specification)作进一步讨论 10.6 异常和继承 对异常规范(exception specification)作进一步讨论 首先,异常规范可以在类成员函数后面指定,与非成员函数一样,成员函数声明的异常规范也是跟在函数参数表的后面。如果成员函数被声明为const或volatile成员函数,则异常规范跟在函数声明的const和volatile限定修饰符之后。 *volatile:表示该函数不做编译优化,优化往往会改变程序执行次序。所以如果对程序执行次序有严格要求,则可加volatile。 第二,如果成员函数在类体外定义,则定义中所指定的异常规范,必须与类定义中该成员函数声明中的类异常规范相同,也就是必须在两处都有相同的异常规范,注意这和函数参数缺省值只能在一处说明(通常在声明中)不同。 第三,虚函数中异常规范可以不同。基类中的虚函数的异常规范,可以与派生类改写的虚函数的异常规范不同。但这不同指的是派生类的虚拟函数的异常规范必须与基类虚函数的异常一样或更严格(是基类虚函数的异常的子集)。之所以要更严格是因为当派生类的虚函数被指向基类类型的指针调用时,保证不会违背基类成员函数的异常规范。
10.6 异常和继承 class CBase{ public: virtual int fun1(int) throw(); 10.6 异常和继承 class CBase{ public: virtual int fun1(int) throw(); virtual int fun2(int) throw(int); virtual string fun3() throw(int,string); }; class CDerived:public CBase{ int fun1(int) throw(int); //错!异常规范不如throw()严格 int fun2(int) throw(int); //对!有相同的异常规范 string fun3() throw(string); //对!异常规范比 throw(int,string)更严格 }
10.7 C++标准库的异常类层次结构 C++标准库中的异常层次的根类被称为exception,定义在库的头文件<exception>中,它是C++标准库函数抛出的所有异常类的基类。 exception类的接口如下: namespace std{ //注意在名字空间域std中 class exception{ public: exception() throw() ; //缺省构造函数 exception(const exception &) throw() ; //拷贝构造函数 exception &operator=(const exception&) throw() ; //拷贝赋值操作符 virtual ~exception() throw() ; //析构函数 virtual const char*what() const throw() ; //返回一个C风格的字符串 }; } 其中虚函数what(),返回一个C风格的字符串,该字符串的目的是为抛出的异常提供文本描述。在前四个函数中都有异常规范throw(),以保证不会在创建、拷贝及撤销exception对象时会抛出异常。
10.7 C++标准库的异常类层次结构 C++标准库提供了一些类,可用在用户编写的程序中,以报告程序的不正常情况。这些预定义的错误被分为两大类:逻辑错误(logic error)和运行时错误(run_time error)。 逻辑错误包括由于程序的内部逻辑而导致的错误或违反了类的不变性的错误 namespace std{ class logic_error:public exception{ public: explicit logic_error(const sting &what_arg); }; class invalid_argment:public logic_error{ public: explicit invalid_argument(const sting &what_arg); }; //如果函数接收到一个无效的实参,就会抛出该异常 class out_of_range:public logic_error{ public: explicit out_of_range(const string &what_arg); }; //如果函数接收到一个不在预期范围中的实参,则抛出该异常 class length_error:public logic_error{ public: explicit length_error(const string &what_arg); }; //用以报告企图产生一个“长度值超出最大允许值”的对象 class domain_error:public logic_error{ public: explicit domain_error(const string &what_arg); }; //domain_error异常,用以报告域错误(domain error)。 }
10.7 C++标准库的异常类层次结构 运行时刻错误是由于程序域之外的事件而引起的错误。运行时刻错误只在程序执行时才是可检测的。运行时刻错误如下: namespace std{ class runtime_error:public exception{ public: explicit runtime_error(const string &what_arg); }; class range_error:public runtime_error{ public: explicit range_over(const string &what_arg); }; //报告内部计算中的范围错误 class overflow_error:public runtime_error{ public: explicit overflow_error(const string &what_arg); }; //报告算术溢出错误 class underflow_error:public runtime_error{ public: explicit underflow_error(const string &what_arg); }; //报告算术下溢错误,以上三个异常是由runtime_error类派生的 class bad_alloc : public exception { public:bad_alloc(const char *_S = "bad allocation") throw(); }; //当new()操作符不能分配所要求的存储区时,会抛出该异常 //它是由基类exception派生的 }
10.7 C++标准库的异常类层次结构 【例10.3】为类模板Array重新定义operator[](),如果索引值越界,那么它会抛出一个out_of_range类型的异常。 using namespace std; const DefaultArraySize=10; //类型缺省为整型 template<typename elemType>class Array{ public: explicit Array(int sz=DefaultArraySize){ size=sz; ia=new elemType [size]; } ~ Array(){delete [] ia;} elemType & operator[](int ix) const{ //对下标运算符[ ]重载 if(ix<0||ix>=size){ //增加异常抛出,防止索引值越界 string eObj="out_of_range error in Array< elemType >::operator[]()"; throw out_of_range(eObj); } return ia[ix]; } //保留原来[ ]的所有索引方式 private: int size; elemType * ia; };
10.7 C++标准库的异常类层次结构 int main(){ int i; Array<int> arr; try{ for(i=0;i<=DefaultArraySize;i++){ arr[i]=i+1; //写入ia[10]时出界 cout<<setw(5)<<arr[i]; } cout<<endl; } catch(const out_of_range & excp){ cerr<<'\n'<<excp.what()<<'\n'; //打印"out_of_range error in Array<elemType>::operator[]()" return -1; } return 0;} 为了使用预定义的异常类,我们的程序必须包含头文件<stdexcept>。传递给out_of_range构造函数的string对象eObj描述了被抛出的异常。当该异常被捕获到时,通过exception类的what()成员函数可以获取这些信息。函数arr[]中的越界索引值将导致Array的operator[]()抛出一个out_of_range类型的异常,它将在main()中被捕获到。
命名空间 命名空间的意义 什么是命名空间 限定名字(别名使用) Using声明与定向 无名的命名空间
命名空间(名字空间)是表达多个变量和多个函数组合成一个组的方法。主要是解决名字(用户定义的类型名、变量名和函数名)冲突的问题。 Namespace 命名空间的名字{ …//各种名字的声明或定义 }
实例分析 namespace You{ #include"iostream.h" namespace My{ int numb; int add(int n) { cout<<n<<endl; return(n++); } namespace You{ int numb; int add(int n) { cout<<n<<endl; return(n++); } void main() { My::numb=My::add(100); You::numb=You::add(10); }
别名的使用 比较短的名字可能会导致冲突,而使用长的命名空间的名字,又不方便。 对长的命名空间的名字,可以使用别名。也可以对标准库的长命名空间的名字取别名。 定义格式:namespace N1=name1; N1是name1的别名。
Using的声明与定向 using声明格式:using NAME::成员 用此方法表示使用的是NAME中的成员 Using 定向格式:using namespace NAME 用此方法表示可以使用NAME中的成员 例『9-3』~『9-5』
无名的命名空间 可以定义一个没有名字的命名空间。 namespace { int num; int fun(); } 相当于: using namespace $$;
异常处理概述 异常处理的基本思想 C++异常处理的实现 标准C++库中的异常类 多路捕获 含有异常的程序设计
在编写程序时,应该考虑确定程序可能出现的错误,然后加入处理错误的代码。也就是说,在环境条件出现异常情况下,不会轻易出现死机和灾难性的后果,而应有正确合理的表现。这就是异常处理。C++提供了异常处理机制,它使得程序出现错误时,力争做到允许用户排除环境错误,继续运行程序。
异常处理概述 程序可能按编程者的意愿终止,也可能因为程序中发生了错误而终止。例如,程序执行时遇到除数为0或下标越界,这时将产生系统中断,从而导致正在执行的程序提前终止。 程序的错误有两种,一种是编译错误,即语法错误。如果使用了错误的语法、函数、结构和类,程序就无法被生成运行代码。另一种是在运行时发生的错误,它分为不可预料的逻辑错误和可以预料的运行异常。
为处理可预料的错误,常用的典型方法是让被调用函数返回某一个特别的值(或将某个按引用调用传递的参数设置为一个特别的值),而外层的调用程序则检查这个错误标志,从而确定是否产生了某一类型的错误。另一种典型方法是当错误发生时跳出当前的函数体,控制转向某个专门的错误处理程序,从而中断正常的控制流。这两种方法都是权宜之计,不能形成强有力的结构化异常处理模式。 异常处理机制是用于管理程序运行期间错误的一种结构化方法。所谓结构化是指程序的控制不会由于产生异常而随意跳转。异常处理机制将程序中的正常处理代码与异常处理代码显式区别开来,提高了程序的可读性。
异常处理的基本思想 对于中小型程序,一旦发生异常,一般是将程序立即中断执行,从而无条件释放系统所有资源。而对于比较大的程序来说,如果出现异常,应该允许恢复和继续执行。恢复的过程就是把产生异常所造成的恶劣影响去掉,中间一般要涉及一系列的函数调用链的退栈,对象的析构,资源的释放等。继续运行就是异常处理之后,在紧接着异常处理的代码区域中继续运行。
C++异常处理的实现 C++语言异常处理机制的基本思想是将异常的检测与处理分离。当在一个函数体中检测到异常条件存在,但无法确定相应的处理方法时,将引发一个异常,并由函数的直接或间接调用检测并处理这个异常。这一基本思想用3个保留字实现:throw、try和catch。其作用是: (1)try:标识程序中异常语句块的开始。 (2)throw:用来创建用户自定义类型的异常错误。 (3)catch:标识异常错误处理模块的开始。
在一般情况下,被调用函数直接检测到异常条件的存在并使用throw引发一个异常(注意,C++语言的异常是由程序员控制引发的,而不是由计算机硬件或程序运行环境控制的);在上层调用函数中使用try检测函数调用是否引发异常,检测到的各种异常由catch捕获并作相应处理。 异常处理的语法 在C++程序中,任何需要检测异常的语句(包括函数调用)都必须在try语句块中执行,异常必须由紧跟着try语句后面的catch语句来捕获并处理。因而,try与catch总是结合使用。throw、try和catch语句的一般语法如下:
throw <表达式>; try { //try语句块 } catch(类型1 参数1) //针对类型1的异常处理 catch (类型2 参数2) //针对类型2的异常处理 … catch (类型n 参数n) //针对类型n的异常处理
异常处理的执行过程如下: (1)控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。 (2)如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从异常被抛掷的try块后跟随的最后一个catch子句后面的语句继续执行下去。 (3)如果在保护段执行期间或在保护段调用的任何函数中有异常被抛掷,则从通过throw运算数创建的对象中创建一个异常对象。编译器从能够处理抛掷类型的异常的更高执行上下文中寻找一个catch子句(或一个能处理任何类型异常的catch处理程序)。catch处理程序按其在try块后出现的顺序被检查。如果没有找到合适的处理程序,则继续检查下一个动态封闭的try块。此处理继续下去直到最外层的封闭try块被检查完。
(4)如果匹配的处理器未找到,则运行函数terminate将被自动调用,而函数terminate的默认功能是调用abort终止程序。 (5)如果找到了一个匹配的catch处理程序,且它通过值进行捕获,则其形参通过拷贝异常对象进行初始化。如果它通过引用进行捕获,则参量初始化为指向异常对象。在形参被初始化之后,开始“循环展开栈”的过程,这包括对那些在与catch处理器相对应的try块开始和异常丢弃地点之间创建的(但尚未析构的)所有自动对象的析构。析构以与构造相反的顺序进行。然后执行catch处理程序,接下来程序跳转到跟随在最后处理程序之后的语句。 注意:catch处理程序的出现顺序很重要,因为在一个try块中,异常处理程序是按照它出现的顺序被检查的。
异常处理的规则 (1)try分程序必须出现在前,catch紧跟出现在后。catch之后的圆括号中必须含有数据类型,捕获是利用数据类型匹配实现的。 (2)如果程序内有多个异常错误处理模块,则当异常错误发生时,系统自动查找与该异常错误类型相匹配的catch模块.查找次序为catch出现的次序。 (3)如果异常错误类型为C++的类,并且该类有其基类,则应该将派生类的错误处理程序放在前面,基类的错误处理程序放在后面。 (4)如果一个异常错误发生后,系统找不到一个与该错误类型相匹配的异常错误处理模块,则调用预定义的运行时刻终止函数,默认情况下是abort。
标准C++库中的异常类 标准C++库中包含9个异常类,它们可以分为运行时异常和逻辑异常: length_error //运行时长度异常 domain_error //运行时域异常 out_of_range_error //运行时越界异常 invalid_argument //运行时参数异常 range_error //逻辑异常,范围异常 overflow_error //逻辑异常,溢出(上)异常 overflow_error //逻辑异常,溢出(下)异常 标准C++库中的这些异常类并没有全部被显式使用,因为C++标准库中很少发生异常,但是这些标准C++库中的异常类可以为编程人员,特别式自己类库的开发者提供一些经验。
多路捕获 很多程序可能有若干不同种类的运行错误,它们可以使用异常处理机制,每种错误可与一个类,一种数据类型或一个值相关。这样,在程序中就会出现多路捕获。
含有异常的程序设计 何时避免异常 异常并不能处理所发生的所有问题。实际上若对异常过分的考虑,将会遇到许多麻烦。下面的段落指出异常不能被保证的情况。 1. 异步事件 2. 普通错误情况 3. 流控制 4. 不强迫使用异常 5. 新异常,老代码
异常的典型使用 1. 随时使用异常规格说明 2. 起始于标准异常 3. 套装用户自己的异常 4. 使用异常层次 5. 多重继承 1. 随时使用异常规格说明 2. 起始于标准异常 3. 套装用户自己的异常 4. 使用异常层次 5. 多重继承 6. 用“引用”而非“值”去捕获 7. 在构造函数中抛出异常 8. 不要在析构函数中导致异常 9. 避免无保护的指针