52
第第第 第第第第 第第第第第第第第第第第第第第第 第第第第第第第第第第第第第第第第第第第第 一。 第第第第第第 第第第 第第第第第第第第第第第第 第第第第第第第第第第第 第第第第第 第第第第第第第第 第第第 ,,,,一 第第第第第第第第第exception handling 第第第 C++ 第第第第第第第第 第第第第第第第第第第第第 第第 try 第第第 第第第 () 第第第第第第第第第第第第第第第 第第第 throw 第 第第 第第 第第第第第第 第第第 catch 第第第 第第第第第 第第第第 第第第第第第第第第 第第第第第第第 第第第第第第第第第第第第第第第第第第第第 第第第第第第第第第第第第第 。,、 第第第 第第第第第第第第第第第第第第第 abort() 第第第第第第第第第第第第 C++ 第第第第第第第第第 第第第第第第第第第第第第第第第第 第 ,一。

第十章 异常处理

  • Upload
    mikko

  • View
    79

  • Download
    4

Embed Size (px)

DESCRIPTION

第十章 异常处理. 大型和十分复杂的程序往往会产生一些很难查找的甚至是无法避免的运行时错误。 当发生运行时错误时,不能简单地结束程序运行,而是退回到任务的起点,指出错误,并由用户决定下一步工作 。 面向对象的异常处理( exception handling )机制是 C++ 语言用以解决这个问题的有力工具。. - PowerPoint PPT Presentation

Citation preview

Page 1: 第十章   异常处理

第十章 异常处理  大型和十分复杂的程序往往会产生一些很难查找的甚至是无法避免的运行时错误。当发生运行时错误时,不能简单地结束程序运行,而是退回到任务的起点,指出错误,并由用户决定下一步工作。面向对象的异常处理( exception handling )机制是 C++ 语言用以解决这个问题的有力工具。函数执行时,放在 try (测试)程序块中的任何类型的数据对象发生异常,都可被 throw 块抛出,随即沿调用链退回,直到被 catch 块捕获,并在此执行异常处理,报告出现的异常等情况。从抛出到捕获,应将各嵌套调用函数残存在栈中的自动对象、自动变量和现场保护内容等进行清除。如果已退到入口函数还未捕获则由 abort() 来终结入口函数。异常处理在 C++ 编程中已经普遍采用,成为提高程序健壮性的重要手段之一。

Page 2: 第十章   异常处理

第十章 异常处理

10.1 异常的概念

10.3 捕获异常

10.2 异常处理的机制 10.5 异常规范

10.7 C++ 标准库异常类 层次结构

10.6 异常和继承

10.4 异常的重新抛出 和 catch_all 子句

Page 3: 第十章   异常处理

10.1 异常的概念    这里所讲的异常( exception )是程序可能检测到 的,运行时不正常的情况,如存储空间耗尽、数组越 界、被 0 除等等,可以预见可能发生在什么地方,但是无法确知怎样发生和何时发生。特别在一个大型的程序(软件)中,程序各部分是由不同的小组编写的,它们由公共接口连起来,错误可能就发生在相互的配合上,也可能发生在事先根本想不到的个别的条件组合上。

本章介绍的技术,尽管是为大型软件工程开发所发展的,但是它在标准 C++ 中已经成为一个标准的技术,在任何规模的程序中都可以使用。 C++ 提供了一些内置的语言特性来产生( raise )或抛出( throw )异常,用以通知“异常已经发生”,然后由预先安排的程序段来捕获( catch )异常,并对它进行处理。这种机制可以在 C++ 程序的两个无关(往往是独立开发)的部分进行“异常”通信。由程序某一部分引发了另一部分的异常,这一异常可回到引起异常的部分去处理(沿着程序函数的调用链)。这也是分清处理责任的好办法。

Page 4: 第十章   异常处理

10.2 异常处理的机制

不再是一测到栈满或空就退出程序了,而是抛出一个异常。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表达式抛出异常为异常处理的第一步。在堆栈的压栈和出栈操作中发生错误而抛出的异常,理所当然地应由调用堆栈的程序来处理。

首先,在 C++ 中异常往往用类( class )来实现,以栈为例,异常类声明如下 :class popOnEmpty{...}; // 栈空异常class pushOnFull{...}; // 栈满异常

Page 5: 第十章   异常处理

10.2 异常处理的机制

请看下面的程序段给出 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 ),分别处理压栈时的栈满和出栈时的栈空。

在 C++ 中建立异常抛出与异常处理之间有一整套程序设计的机制。首先采用关键字 try ,构成一个 try 块( try block ),它包含了抛出异常的语句。当然也可以是包含了这样的调用语句,该语句所调用的函数中有能够抛出异常的语句。

Page 6: 第十章   异常处理

10.2 异常处理的机制

程序按下列规则控制: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() 。

由 catch 字句捕获并处理异常是第二步。注意与 catch 语句分别匹配的是在压栈和出栈成员函数模板中的 throw 语句,一个抛出 pushOnFull 类的无名对象,另一个抛出 popOnEmpty 类的无名对象。

在编制程序时有一条惯例:把正常执行的程序与异常处理两部分分隔开来,这样使代码更易于跟随和维护。在上例中,我们可以把两个 try 块合成一个,而把两个 catch 子句都放在函数最后。

Page 7: 第十章   异常处理

10.2 异常处理的机制

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 子句捕获了一个异常,并转到该块中进行异常处理。

把程序的正常处理代码和异常处理代码分离的最清楚的方法是定义函数 try 块( function try block ) , 但 VC++6.0 不支持。这种方法是把整个函数包括在 try 块中:

Page 8: 第十章   异常处理

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 保存那些不能被压入栈中的值。该值即调用构造函数时的实参。

Page 9: 第十章   异常处理

10.3 捕获异常

这样在 catch 子句中,要取得 _value ,须调用 pushOnFull 中的成员函数 value() :catch ( pushOnFull<T> eObj ) { cerr<<” 栈满” <<eObj.value()<<” 未压入栈” <<endl; return 1;}在 catch 子句的异常声明中声明了对象 eObj ,用它来调用 pushOnFull 类的对象成员函数 value() 。异常对象是在抛出点被创建,与catch 子句是否显式要求创建一个异常对象无关,该对象总是存在,在 catch 子句中只是为了调用异常处理对象的成员函数才声明为对象,不用类。

对应在 throw 表达式中,构造抛出对象也要有实参:throw pushOnFull(data); //data 即 Push(const &data) 中的参数 data

*catch 子句异常声明中采用对象只是一种形式。甚至异常并非一个类对象时,也可以用同样的格式,比如异常为一枚举量,这时就等效于按值传递,而不是调用类对象的公有成员。

Page 10: 第十章   异常处理

10.3 捕获异常catch 子句的异常声明与函数参数声明类似,可以是按值传送,也可以是按引用传递。如果 catch 子句的异常声明改为引用声明,则 catch 子句可以直接引用由 throw 表达式创建的异常对象,而不必创建自己的局部拷贝。对大型类对象减少不必要

的拷贝是很有意义的,所以对于类类型的异常,其异常声明最好也是被声明为引用。如:catch(pushOnFull<T> & eObj){ cerr<<” 栈满” <<eObj.value()<<” 未压栈” <<endl; return 1; }使用引用类型的异常声明, catch 子句能够修改异常对象,但仅仅是异常对象本身,正常程序部分的量并不会被修改。与一般类对象不同,实际上异常对象处理完后,生命期也就结束了。只有需要重新抛出异常(在下一节中讨论),修改操作才有意义。

下面的讨论展现了 C++ 异常处理的不可替代的技术。

Page 11: 第十章   异常处理

寻找匹配的 catch 子句有固定的过程:如果 throw 表达式位于 try 块中,则检查与 try 块相关联的 catch 子句列表,看是否有一个子句能够处理该异常,有匹配的,则该异常被处理;找不到匹配的 catch子句,则在主调函数中继续查找。如果一个函数调用在退出时带有一个被抛出的异常未能处理,而且这个调用位于一个try 块中,则检查与该 try 块相关联的 catch 子句列表,看是否有一个子句匹配,有,则处理该异常;没有,则查找过程在该函数的主调函数中继续进行。即这个查找过程逆着嵌套的函数调用链向上继续,直到找到处理该异常的 catch子句。只要遇到第一个匹配的catch 子句,就会进入该 catch 子句,进行处理,查找过程结束。

Page 12: 第十章   异常处理

10.3 捕获异常

  在栈异常处理的例子中,对 popOnEmpty ,首先应在 istack 的成员函数 Pop() 中找,因为 Pop() 中的 throw 表达式没有在 try 块中,所以 Pop() 带着一个异常退出。下一步是检查调用 Pop() 的函数,这里是 main() ,在 main() 中对Pop() 的调用位于一个 try 块中,则可用与该 try 块关联的 catch 子句列表中的某一个来处理,找到第一个 popOnEmpty 类型异常声明的 catch 子句,并进入该子句进行异常处理。

因发生异常而逐步退出复合语句和函数定义,被称为栈展开( stack unwinding )。这是异常处理的核心技术。

异常对程序的影响通常不仅是在发生异常的那个局部范围中,而且可能逆调用链而上,甚至整个任务。因此,异常处理应该在其对程序影响的终结处进行,甚至是在调用该任务的菜单处进行。

Page 13: 第十章   异常处理

10.3 捕获异常 在栈展开期间,当一个复合语句(或语句块)或函数退出时,在退出的域中有某个局部量是类对象,栈展开过程将自动调用该对象的析构函数,完成资源的释放。所以 C++ 异常处理过程本质上反映的是“资源获取是由构造函数实现,而资源释放是由析构函数完成”这样一种程序设计技术。采用面向对象的程序设计,取得资源的动作封装在类的构造函数中,释放资源的动作封装在类的析构函数中,当一个函数带着未处理的异常退出时,函数中这种类对象被自动销毁,资源(包括动态空间分配的资源和打开的文件)释放。栈展开过程决不会跳过封装在类的析构函数中的资源释放动作。所以由文件重构对象应该放在构造函数中,而把对象存入文件应该放在析构函数中。

退出调用链时必须释放所有资源,由系统回收。

异常处理应该用于面向对象的程序设计。对非面向对象的程序设计如果函数动态获得过资源,因异常,这些资源的释放语句可能被忽略,则这些资源将永远不会被自动释放。

Page 14: 第十章   异常处理

10.3 捕获异常

  异常不能够保持在未被处理的状态。异常表示一个程序不能够继续正常执行,这是非常严重的问题,如果没有找到处理代码,程序就调用 C++ 标准库中定义的函数 terminate() 。 terminate() 的缺省行为是调用 abort() ,指示从程序中非正常退出。

异常对象是在 throw 表达式中建立并抛出: throw 表达式通过调用异常类的构造函数创建一个临时对象,然后把这个临时对象拷贝到一个被称为异常对象( exception object )的存贮区中,它保证会持续到异常被处理完。

函数调用和异常处理之间的异同: throw 表达式的行为有点像函数的调用,而 catch 子句有点像函数定义。函数调用和异常处理的主要区别是:建立函数调用所需要的全部信息在编译时已经获得,而异常处理机制要求运行时的支持。对于普通函数调用,通过函数重载解析过程,编译器知道在调用点上哪个函数会真正被调用。但对于异常处理,编译器不知道特定的 throw 表达式的 catch 子句在哪个函数中,以及在处理异常之后执行权被转移到哪儿。这些都在运行时刻决定,异常是随机发生的,异常的处理的 catch 子句是沿调用链逆向进行查找,这与运行时的多态 —— 虚函数也是不一样的。当一个异常不存在处理代码时,编译器无法通知用户,所以要有terminate() 函数,它是一种运行机制,当没有处理代码( catch 子句)能够匹配被抛出的异常时由它通知用户。

Page 15: 第十章   异常处理

10.4 异常的重新抛出和 catch_all 子句

rethrow表达式仍为:throw; 但仅有一个关键字,因为异常类型在 catch 语句中已经有了,不必再指明。被重新抛出的异常就是原来的异常对象。但是重新抛出异常的 catch 子句应该把自己做过的工作告诉下一个处理异常的 catch 子句,往往要对异常对象做一定修改,以表达某些信息,因此 catch 子句中的异常声明必须被声明为引用,这样修改才能真正做在异常对象自身中,而不是拷贝中。

当 catch 语句捕获一个异常后,可能不能完全处理异常,完成某些操作后,该异常必须由函数链中更上级的函数来处理,这时 catch 子句可以重新抛出( rethrow )该异常,把异常传递给函数调用链中更上级的另一个 catch 子句,由它进行进一步处理。

Page 16: 第十章   异常处理

10.4 异常的重新抛出和 catch_all 子句

  因为我们不知道可能被抛出的全部异常,所以不是为每种可能的异常写一个 catch 子句来释放资源,而是使用通用形式的 catch 子句 catch_all :catch(...){ 代码 */} 对任何异常都可以进入这个 catch 子句。这里的三个点称为省略号。花括号中的复合语句用来执行指定操作,当然可以包括资源的释放。

通常异常发生后按栈展开( stack unwinding )退出,动态分配的非类对象资源是不会自动释放的,应该在对应的 catch 子句中释放。

void fun1(){ int *res; res=new int[100]; // 定义一个资源对象 try{ // 代码包括使用资源 res 和某些可能引起异常抛出的操作,异常可能有多种 }catch(...){ // 不论是那种异常都在此释放 delete [] res; // 释放资源对象 res throw; // 重新抛出异常 }delete [] res; // 正常退出前释放资源对象 res;}

Page 17: 第十章   异常处理

10.4 异常的重新抛出和 catch_all 子句

   catch_all 子句可以单独使用,也可以与其它 catch子句联合使用。如果联合使用,它必须放在相关 catch子句表的最后。因为 catch 子句被检查的顺序与它们在try 块之后排列顺序相同,一旦找到了一个匹配,则后续的 catch 子句将不再检查,按此规则, catch_all 子句( catch(...){} )处理表前面所列各种异常之外的异常。如果只用 catch_all 子句进行某项操作,则其他的操作应由 catch 子句重新抛出异常,沿调用链逆向去查找新的处理子句来处理,而不能在子句列表中再按排一个处理同一异常的子句,因为第二个子句是永远执行不到的。

Page 18: 第十章   异常处理

10.5 异常规范

一个函数的异常规范的违例只能在运行时才能被检测出来。如果在运行时,函数抛出了一个没有被列在它的异常规范中的异常时,则系统调用C++ 标准库中定义的函数 unexpected() 。必须进一步指出,仅当函数中所抛出的异常,没有在该函数内部处理,而是沿调用链回溯寻找匹配的 catch 子句的时候 , 异常规范才起作用。在函数指针的声明中也可给出一个异常规范,它所指向的函数也必须有同样的异常规范,或者是其中的一部分(子集)。如果异常规范为 throw() ,则表示不得抛出任何异常。必须指出 VC++6.0 不支持异常规范,编程可以包括异常规范,实际什么也未做 。

异常规范( exception specification )提供了一种方案,可以随着函数声明列出该函数可能抛出的异常,并保证该函数不会抛出任何其他类型的异常,在 stack 类定义中可有: void Push(const T&data) throw(pushOnFull);T Pop() throw(popOnEmpty); 如果成员函数是在类外定义,则类内声明和类外定义必须都有同样的异常规范。

Page 19: 第十章   异常处理

public: pushOnFull(T i){_value=i;} T value(){return _value;} void print(){cerr<<” 栈满,” <<value()<<” 未压入栈” <<endl;} };template<typename T>class popOnEmpty{public: void print(){cerr<<” 栈已空,无法出栈” <<endl;} }; ......template<typename T>class Stack{ ......public: ...... void Push(const T&data) throw(pushOnFull<T>); T Pop() throw(popOnEmpty<T>); ...... }

学习了前几节内容后,给出下例作为小结【例 10.1】包含栈满或空异常的较完整的程序段。.......template<typename T>class pushOnFull{ T _value;

Page 20: 第十章   异常处理

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; }

template<typename T> void Stack<T>::Push(const T &data) throw(pushOnFull<T>){ if(IsFull()) throw pushOnFull<T>(data); // 栈满则抛出异常

10.5 异常规范

Page 21: 第十章   异常处理

10.6 异常和继承

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{...}; // 数学库被零除异常

在 C++ 程序中,表示异常的类通常被组成为一个组(即如在前面各节讨论的那样)或者一个层次结构。对由栈类成员函数抛出的异常 , 可以定义一个称为 Excp 的基类 :

Page 22: 第十章   异常处理

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 子句必须先出现。

Page 23: 第十章   异常处理

10.6 异常和继承

重新抛出的异常仍是原来的异常对象。如果程序中抛出了 pushOnFull 类类型的异常,而它被基类的 catch 子句处理,并在其中再次被抛出,那么这个异常仍是 pushOnFull 类类型的异常,而不是其基类类型的异常。

类层次结构的异常同样可以重新抛出( rethrow ),把一个异常传递给函数调用列表中,更上层的另一个 catch 子句:throw;

在基类 catch 子句处理的是异常对象的基类子对象的一份拷贝,该拷贝只在该 catch 子句中被访问,重新抛出的是原来的异常对象。这个放在异常对象存储区中的异常的生命期应该是在处理该异常的一系列的子句中最后一个退出时才结束,也就是直到这时,才由异常类的析构函数来销毁它。这一系列的子句是由重新抛出联系起来的。虚函数是类层次结构中多态性的基本手段,异常类层次结构中也可以定义虚拟函数。

Page 24: 第十章   异常处理

10.6 异常和继承

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;} };

【例 10.2】异常层次结构中的虚函数。为了调用派生类对象的虚拟函数,异常声明必须为一个指针或引用

int main(){ try{ // 抛出一个 pushOnFulll 异常 } catch(Excp&eObj){ eObj.print(); } // 调用虚函数 pushOnFull::print() try{ // 抛出一个 popOnEmpty 异常 } catch(Excp&eObj){ eObj.print(); } // 调用虚函数 pushOnFull::print()}

catch 子句输出为:栈满,不能压栈栈已空,无法出栈

Page 25: 第十章   异常处理

10.6 异常和继承

   首先,异常规范可以在类成员函数后面指定,与非成员函数一样,成员函数声明的异常规范也是跟在函数参数表的后面。如果成员函数被声明为 const 或 volatile 成员函数,则异常规范跟在函数声明的 const 和 volatile 限定修饰符之后。*volatile :表示该函数不做编译优化,优化往往会改变程序执行次序。所以如果对程序执行次序有严格要求,则可加 volatile 。

对异常规范( exception specification )作进一步讨论

第二,如果成员函数在类体外定义,则定义中所指定的异常规范,必须与类定义中该成员函数声明中的类异常规范相同,也就是必须在两处都有相同的异常规范,注意这和函数参数缺省值只能在一处说明(通常在声明中)不同。第三,虚函数中异常规范可以不同。基类中的虚函数的异常规范,可以与派生类改写的虚函数的异常规范不同。但这不同指的是派生类的虚拟函数的异常规范必须与基类虚函数的异常一样或更严格(是基类虚函数的异常的子集)。之所以要更严格是因为当派生类的虚函数被指向基类类型的指针调用时,保证不会违背基类成员函数的异常规范。

Page 26: 第十章   异常处理

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{public: int fun1(int) throw(int); // 错!异常规范不如 throw()严格 int fun2(int) throw(int); // 对!有相同的异常规范 string fun3() throw(string); // 对!异常规范比 throw(int,string) 更严格 }

Page 27: 第十章   异常处理

10.7 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风格的字符串 }; }

C++ 标准库中的异常层次的根类被称为 exception ,定义在库的头文件 <exception> 中,它是 C++ 标准库函数抛出的所有异常类的基类。

其中虚函数 what() ,返回一个 C风格的字符串,该字符串的目的是为抛出的异常提供文本描述。在前四个函数中都有异常规范 throw() ,以保证不会在创建、拷贝及撤销 exception 对象时会抛出异常。

Page 28: 第十章   异常处理

10.7 C++ 标准库的异常类层次结构

逻辑错误包括由于程序的内部逻辑而导致的错误或违反了类的不变性的错误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 )。}

C++ 标准库提供了一些类,可用在用户编写的程序中,以报告程序的不正常情况。这些预定义的错误被分为两大类:逻辑错误( logic error )和运行时错误( run_time error )。

Page 29: 第十章   异常处理

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派生的}

运行时刻错误是由于程序域之外的事件而引起的错误。运行时刻错误只在程序执行时才是可检测的。运行时刻错误如下:

Page 30: 第十章   异常处理

10.7 C++ 标准库的异常类层次结构

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.3 】为类模板 Array 重新定义 operator[]() ,如果索引值越界,那么它会抛出一个 out_of_range 类型的异常。

Page 31: 第十章   异常处理

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() 中被捕获到。

Page 32: 第十章   异常处理

命名空间 命名空间的意义 什么是命名空间 限定名字(别名使用) Using 声明与定向 无名的命名空间

Page 33: 第十章   异常处理

命名空间(名字空间)是表达多个变量和多个函数组合成一个组的方法。主要是解决名字(用户定义的类型名、变量名和函数名)冲突的问题。

Namespace 命名空间的名字 { …// 各种名字的声明或定义 }

Page 34: 第十章   异常处理

实例分析 #include"iostream.h" namespace My{

int numb; int add(int n) { co

ut<<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);}

Page 35: 第十章   异常处理

别名的使用 比较短的名字可能会导致冲突,而使用

长的命名空间的名字,又不方便。 对长的命名空间的名字,可以使用别名。

也可以对标准库的长命名空间的名字取别名。

定义格式: namespace N1=name1; N1 是 name1 的别名。

Page 36: 第十章   异常处理

Using 的声明与定向 using 声明格式: using NAME:: 成员 用此方法表示使用的是 NAME 中的成员 Using 定向格式: using namespace NA

ME 用此方法表示可以使用 NAME 中的成员 例『 9-3 』~『 9-5 』

Page 37: 第十章   异常处理

无名的命名空间 可以定义一个没

有名字的命名空间。

namespace { int num; int fun(); } 相当于:

namespace $${ int num; int fun(); }using namespace $$;

Page 38: 第十章   异常处理

异常处理 异常处理概述 异常处理的基本思想 C++ 异常处理的实现 标准 C++ 库中的异常类 多路捕获 含有异常的程序设计

Page 39: 第十章   异常处理

在编写程序时,应该考虑确定程序可能出现的错误,然后加入处理错误的代码。也就是说,在环境条件出现异常情况下,不会轻易出现死机和灾难性的后果,而应有正确合理的表现。这就是异常处理。 C++ 提供了异常处理机制,它使得程序出现错误时,力争做到允许用户排除环境错误,继续运行程序。

Page 40: 第十章   异常处理

异常处理概述程序可能按编程者的意愿终止,也可能因为程

序中发生了错误而终止。例如,程序执行时遇到除数为 0 或下标越界,这时将产生系统中断,从而导致正在执行的程序提前终止。

程序的错误有两种,一种是编译错误,即语法错误。如果使用了错误的语法、函数、结构和类,程序就无法被生成运行代码。另一种是在运行时发生的错误,它分为不可预料的逻辑错误和可以预料的运行异常。

Page 41: 第十章   异常处理

为处理可预料的错误,常用的典型方法是让被调用函数返回某一个特别的值(或将某个按引用调用传递的参数设置为一个特别的值),而外层的调用程序则检查这个错误标志,从而确定是否产生了某一类型的错误。另一种典型方法是当错误发生时跳出当前的函数体,控制转向某个专门的错误处理程序,从而中断正常的控制流。这两种方法都是权宜之计,不能形成强有力的结构化异常处理模式。

异常处理机制是用于管理程序运行期间错误的一种结构化方法。所谓结构化是指程序的控制不会由于产生异常而随意跳转。异常处理机制将程序中的正常处理代码与异常处理代码显式区别开来,提高了程序的可读性。

Page 42: 第十章   异常处理

异常处理的基本思想对于中小型程序,一旦发生异常,一般是将程

序立即中断执行,从而无条件释放系统所有资源。而对于比较大的程序来说,如果出现异常,应该允许恢复和继续执行。恢复的过程就是把产生异常所造成的恶劣影响去掉,中间一般要涉及一系列的函数调用链的退栈,对象的析构,资源的释放等。继续运行就是异常处理之后,在紧接着异常处理的代码区域中继续运行。

Page 43: 第十章   异常处理

C++ 异常处理的实现C++ 语言异常处理机制的基本思想是将异常的

检测与处理分离。当在一个函数体中检测到异常条件存在,但无法确定相应的处理方法时,将引发一个异常,并由函数的直接或间接调用检测并处理这个异常。这一基本思想用 3 个保留字实现: throw 、 try 和 catch 。其作用是:

( 1 ) try :标识程序中异常语句块的开始。 ( 2 ) throw :用来创建用户自定义类型的异

常错误。( 3 ) catch :标识异常错误处理模块的开始。

Page 44: 第十章   异常处理

在一般情况下,被调用函数直接检测到异常条件的存在并使用 throw引发一个异常(注意, C++ 语言的异常是由程序员控制引发的,而不是由计算机硬件或程序运行环境控制的);在上层调用函数中使用 try检测函数调用是否引发异常,检测到的各种异常由 catch 捕获并作相应处理。

异常处理的语法 在 C++ 程序中,任何需要检测异常的语句(包括函数调用)都必须在 try 语句块中执行,异常必须由紧跟着 try 语句后面的 catch 语句来捕获并处理。因而,try 与 catch总是结合使用。 throw 、 try 和 catch 语句的一般语法如下:

Page 45: 第十章   异常处理

throw < 表达式 >;try{ //try 语句块}catch (类型 1 参数 1 ){ //针对类型 1 的异常处理}catch (类型 2 参数 2 ){ //针对类型 2 的异常处理}…catch (类型 n 参数 n ){ //针对类型 n 的异常处理}

Page 46: 第十章   异常处理

( 1 )控制通过正常的顺序执行到达 try 语句,然后执行 try块内的保护段。

( 2 )如果在保护段执行期间没有引起异常,那么跟在 try块后的 catch子句就不执行,程序从异常被抛掷的 try块后跟随的最后一个 catch子句后面的语句继续执行下去。

( 3 )如果在保护段执行期间或在保护段调用的任何函数中有异常被抛掷,则从通过 throw 运算数创建的对象中创建一个异常对象。编译器从能够处理抛掷类型的异常的更高执行上下文中寻找一个 catch子句(或一个能处理任何类型异常的 catch 处理程序)。 catch 处理程序按其在 try块后出现的顺序被检查。如果没有找到合适的处理程序,则继续检查下一个动态封闭的 try块。此处理继续下去直到最外层的封闭 try块被检查完。

异常处理的执行过程如下:

Page 47: 第十章   异常处理

( 4 )如果匹配的处理器未找到,则运行函数 terminate 将被自动调用,而函数 terminate 的默认功能是调用 abort终止程序。

( 5 )如果找到了一个匹配的 catch 处理程序,且它通过值进行捕获,则其形参通过拷贝异常对象进行初始化。如果它通过引用进行捕获,则参量初始化为指向异常对象。在形参被初始化之后,开始“循环展开栈”的过程,这包括对那些在与 catch 处理器相对应的 try块开始和异常丢弃地点之间创建的(但尚未析构的)所有自动对象的析构。析构以与构造相反的顺序进行。然后执行 catch 处理程序,接下来程序跳转到跟随在最后处理程序之后的语句。

注意: catch 处理程序的出现顺序很重要,因为在一个 try块中,异常处理程序是按照它出现的顺序被检查的。

Page 48: 第十章   异常处理

异常处理的规则( 1 ) try 分程序必须出现在前, catch紧跟出现在后。

catch之后的圆括号中必须含有数据类型,捕获是利用数据类型匹配实现的。

( 2 )如果程序内有多个异常错误处理模块,则当异常错误发生时,系统自动查找与该异常错误类型相匹配的 catch模块 .查找次序为 catch 出现的次序。

( 3 )如果异常错误类型为 C++ 的类,并且该类有其基类,则应该将派生类的错误处理程序放在前面,基类的错误处理程序放在后面。

( 4 )如果一个异常错误发生后,系统找不到一个与该错误类型相匹配的异常错误处理模块,则调用预定义的运行时刻终止函数,默认情况下是 abort 。

Page 49: 第十章   异常处理

标准 C++ 库中的异常类标准 C++ 库中包含 9 个异常类,它们可以分为运行时异常和逻辑异常:

length_error // 运行时长度异常domain_error // 运行时域异常out_of_range_error // 运行时越界异常invalid_argument // 运行时参数异常range_error // 逻辑异常,范围异常overflow_error // 逻辑异常,溢出(上)异常

overflow_error // 逻辑异常,溢出(下)异常 标准 C++ 库中的这些异常类并没有全部被显式使用,因为 C++ 标准库中很少发生异常,但是这些标准 C++ 库中的异常类可以为编程人员,特别式自己类库的开发者提供一些经验。

Page 50: 第十章   异常处理

多路捕获很多程序可能有若干不同种类的运行错误,它

们可以使用异常处理机制,每种错误可与一个类,一种数据类型或一个值相关。这样,在程序中就会出现多路捕获。

Page 51: 第十章   异常处理

含有异常的程序设计何时避免异常

异常并不能处理所发生的所有问题。实际上若对异常过分的考虑,将会遇到许多麻烦。下面的段落指出异常不能被保证的情况。

1. 异步事件2. 普通错误情况3. 流控制4. 不强迫使用异常5. 新异常,老代码

Page 52: 第十章   异常处理

异常的典型使用1. 随时使用异常规格说明2. 起始于标准异常3. 套装用户自己的异常4. 使用异常层次5. 多重继承6. 用“引用”而非“值”去捕获7. 在构造函数中抛出异常8. 不要在析构函数中导致异常9. 避免无保护的指针