2023年11月29日发(作者:)

C++程序设计

15 异常

程序中经常要检查处理各种错误情形,如果用传统的流程控制语句来处理,很容易使程

序逻辑混乱。异常(exception)就是一种专门用于检测错误并处理的一种机制,使程序保持逻

辑清晰,并改进程序的可靠性。C++语言提供了基本的异常处理机制。本章主要介绍异常的

概念、语句、异常类型架构及应用。

可靠的编程应尽可能地、及时地检测到各种异常情形,尽可能在本地处理。尽管有时自

己不能处理,也应该向调用方提供详细的出错信息,使调用方能得到充分信息,从而采取合

适方式来处理异常。

15.1 异常的概念

异常是什么概念?异常就是在程序运行中发生的难以预料的、不正常的事件而导致偏离

正常流程的现象。例如:

访问数组元素的下标越界,在越界时又写入了数据;

new动态申请内存而返回空指针(可能是因内存不足)

算术运算上溢出或下溢出;

整数除法中除数为0

调用函数时提供了无效实参,如指针实参为空指针(如用空指针来调用strlen函数)

通过挂空指针或挂空引用来访问对象;

输入整数或浮点数失败;

I/O错误,等等。

上面列出的情形之一如果发生,就可能导致运行错误而终止程序。

发生异常将导致正常流程不能进行,就需要对异常进行处理。那么异常处理是什么概

念?异常处理(exception handling)就是在运行时刻对异常进行检测、捕获、提示、传递等过

程。如果采用传统的if-else语句来检测处理所有可能发生的异常,很容易导致程序流程混乱,

分不清正常流程与异常处理,而且在处理一个异常时往往又引入了新的异常。

假设要设计一个函数,从一个文本文件中读取数据得到一个float矩阵。该文件应存放

一个m*nfloat矩阵,头两个整数说明其行数m和列数n。你要把它读入并创建一个矩阵

对象,以备下一步计算。如果你认为文本文件不会有错,完全按正常编程,不超过10条语

句就能完成。如果这个文本文件是别人提供的,而且你的函数将提供给其它人使用,那么你

在每一步都要考虑可能出现的错误,此时就可能需要30条语句来处理。例如,可能的出错

情形如下:

打开文件出错,文件名可能有误;

读取行数m或者列数n可能出错;

读取每个元素时都可能出错;

矩阵数据可能不完整,也会出错。

如果你用传统方式来判断处理以上这些问题,就会发现正常的流程被淹没在多种异常判

断处理之中。此时就需要有一种统一的机制能将正常流程与异常处理分开描述,而保持程序

逻辑清晰可读,同时各种异常情形能被集中处理。

C++提供了引发异常语句throw和捕获处理异常语句try-catch。它们构成了一种特殊的

流程控制。

throw引发的每个异常都可以描述为一个对象或一个值。在程序中,每一种异常都可

以描述为一种类型,可能是自定义的类,也可能是简单的整数或字符串。在比较完善的编程

中,经常用不同的类来描述不同的异常,建立一个异常类型的继承结构,以方便对异常类型

的管理和重用。

一个函数中当检测到某种异常发生,但自己往往不知道应该如何处理,此时就应该通知

调用方知道发生了什么异常。处理异常的一般方式是:在一个函数中发现一个错误但不能处

理,就用throw语句引发一个异常,希望它的(直接或间接)调用方能够捕获并处理这个异

常。函数的调用方如果能解决该异常,就可使用try-catch语句来捕获并处理这种异常。如

果调用方不能捕获处理该异常,异常就被传递到它自己的调用方,最后到达main函数。

异常的发生、传递与处理的过程与函数调用堆栈相关。如图15.1所示。main函数中调

g(){...return;}g(){...throw x;}

callreturncallthrow

f(){...g();...return;}f(){...g();...}

callreturncallthrow

main(){...f();...}main(){...f();...}

(a)正正正正正正正(b)正正正正正正正正正

15.1 函数调用堆栈与异常传递

f函数,f函数再调用g函数。如果g函数执行return就正常返回到f如果f执行到return

就正常返回到main。这是正常流程。

如果g函数在运行时因检测到某种错误而用throw语句引发一个异常,而自己也没有捕

获处理,此时该异常就被传递到f的调用方g函数,而且g函数执行终止(注意,不是返回)

对于f来说就是g函数调用发生异常。此时如果f函数没有捕获该异常,那么异常又被传递

到它的调用方main函数,此时f函数执行终止。同理,此时如果main也没有捕获该异常,

那么程序就必须终止。此时系统可能会跳出一个对话框告知你发生了运行错误。

在发生异常、传递异常的过程中,如果有一个函数用try-catch捕获了该异常,就不会

导致程序终止。在运行时刻,一个异常只能被捕获一次。假设f函数捕获了这个异常,那么

对于它的调用方main函数来说,就等于没有发生异常。

异常编程的目的是改善程序的可靠性。在大型复杂程序中,完全不发生异常几乎不可能,

用传统的if-else语句来检查所有可能的异常情形,也有很大困难。编程正确性总是依赖某些

假设成立为前提,异常编程就是要分析识别这些假设不成立的情形,采用面向对象编程技术,

建立各种异常类型并形成继承性架构,以处理程序中可能发生的各类异常。

15.2 异常类型的架构

C++的异常类型可以是任何类型,既可以是基本类型,如int整数、char*字符串,也可

以是自定义类型。在比较规范的编程中,往往不能将基本类型作为异常类型。这是因为基本

类型所能表示的异常种类有限。例如在一个程序中int类型只能表示一种异常情形,如果在

不同函数中多处引发不同语义的int异常,就很难区别不同int值的含义。可能表示访问数

组的下标越界,也可能表示打开文件不成功。

在比较规范的编程中往往根据各种错误情形,利用类的继承性建立一个异常类型的架

构,作用如下:

对所处理的各种错误情形进行准确描述、抽象和归类。

方便扩展新的异常类型。

在编程中方便选取引发正确的异常类型,也方便按类型来捕获处理异常。

15.2是定义在中的一个异常类型架构。在头文件

exception

logic_errorruntime_error. . .

domain_error

invalid_argument

out_of_range

range_errorunderflow_error

overflow_error

length_error

15.2 异常类型架构

中定义了基类exception和一组函数,在中定义了一组派生类,表示各类具体的

异常。标准模板库STL中的部分函数就利用了这个架构。下面简单介绍各种异常类型。

exception是所有异常类的基类,其公共成员如下:

class exception{

...

public:

exception() throw(); //缺省构造函数

exception(const exception& rhs) throw(); //拷贝构造函数

exception& operator=(const exception& rhs) throw();//赋值操作函数

virtual ~exception() throw(); //虚析构函数

virtual const char *what() const throw(); //虚函数

};

注意到每个函数原型末尾都有throw()称为函数的异常规范(exception-specification)

括号中为空说明该函数中不会引发任何异常出来。如果一个函数在执行时可能引发某种异常

类型,就应该在“thow(异常类型表)”中说明,以告知调用方。

每个异常对象都至少包含一个字符串,来说明异常发生的原因或出错性质,称为出错信

息。因此大多派生异常类都提供含字符串形参的构造函数。例如:

class invalid_argument : public logic_error{

public:

invalid_argument(const string& what_arg); //构造函数

};

一般地,派生类继承基类的虚函数what(),返回出错信息。如果需要的话,派生类可以

改写这个虚函数,以提供更多信息。

logic_error表示逻辑错误的异常类型,此类错误是在特定代码执行之前就违背了某些

前置条件,例如,数据越界out_of_range,函数调用实参无效invalid_argument等,也包括

特定领域相关的错误domain_error。读者可自行扩展新类型。例如,访问数据的下标越界可

作为out_of_range的派生类。一些逻辑错误意味着编程有误,一般通过改进编程能避免。

runtime_error表示运行期错误,在程序执行期间才能检测的错误。例如算术运算可

能导致上溢出overflow_error、下溢出underflow_error、数值越界range_error等。读者可自

行扩展新的类型,如空指针错误可作为runtime_error的派生类。一些运行期错误有一定偶

然性,与执行环境有关,如内存不足、打开文件失败等,此类错误并不能通过改进自身编程

来消除。

一个异常类所包含的信息越多,对于此类错误的检测和处理就越有利。例如,要说明下

标越界错误,就应该说明该下标的当前值是多少,可能的话,还应说明合理的下标范围。这

需要添加新的数据成员以及相应的成员函数。例如:

class Index_out_of_range : public out_of_range{

const int index;

public:

Index_out_of_range(int index1, const string& what_arg)

:index(index1), out_of_range(what_arg){}

int getIndex()const {return index;}

};

再如,要说明读取文件到特定位置时发生数据错误,就应该说明文件名、出错位置、所

读到的数据等信息。后面将详细介绍这些派生类的设计。

大多数异常派生类都很简短,关键是对异常的识别和命名。

C++异常分为两类:有命名的和未命名的。有命名的异常是有类型的,基本类型(int)

或字符串(char *)或自定义类型。未命名的异常是在运行时刻某种底层错误引起的,例如,

整数相除时除数为0,通过挂空指针或挂空引用来访问对象,破坏当前函数的堆栈使函数返

回到错误地址等。未命名异常虽然也能被捕获,但不能提供确切的出错信息。建立异常类型

架构本质上就是对各种异常情形的识别与命名,对于异常处理具有重要作用。

15.3 异常处理语句

C++语言的异常处理语句包括引发异常语句throw和捕获处理语句try-catch

15.3.1 throw语句

引发异常语句的语法格式为:

throw <表达式>;

其中,关键字throw表示要引发一个异常到当前作用域之外。表达式值的类型作为异常

事件的类型,并将表达式的值传给捕获处理该类型异常的程序。表达式的值可能是一个基本

类型的值,也可能是一个对象。如果要引发一个对象,对象类应该事先设计好。一个类表示

了一种异常事件,应描述该类异常发生的原因、语境以及可能的处理方法等。

如果在一个函数编程中发现了自己不能处理的错误情形,就可使用throw语句引发一个

异常,将它引发到当前作用域之外。如果当前作用域是一个函数,就将异常传递给函数的调

用方,让调用方来处理。

throwreturn相似,表达式也相似,都会中止后面代码的执行。throw语句执行将控

制流转到异常捕获语句处理。这将导致throw语句下面相邻语句不能执行,而且会自动回收

当前作用域中的局部变量。例如:

throw index; //引发一个int异常,index是一个int变量

//引发一个const char *异常 throw "index out of range";

throw invalid_argument("denominator is zero");//引发invalid_argument异常

最后一个throw语句执行过程是,先创建一个invalid_argument对象,然后再将该对象

引发到当前作用域之外。

throwreturn的含义不同。一个函数的返回值表示正常执行的结果,要作为显式说明

的函数规范。throw语句虽然也能终止当前函数的执行,但表示不正常的执行结果。一个函

数只有一种返回类型,但可能引发多种类型的异常。

为了说明一个函数可能引发哪些类型的异常,可用异常规范(exception specification)来说

明,就是“throw(异常类型表)。例如下面是一个求商函数:

double quotient(int numrator, int denominator) throw(invalid_argument){

if (denominator == 0)

throw invalid_argument("denominator is zero");

return double(numrator) / denominator;

}

该函数的第一个形参除以第二个形参,返回商作为结果。该函数的原型中包含了异常规

范:,说明该函数的调用可能引发invalid_argument异常。函数

throw(invalid_argument)

体中检查第二个形参(即除数),如果除数为0,就引发该异常。

尽管异常规范目前还起不到语法检验的作用,但起码能告知函数的调用方注意捕获哪些

类型的异常,而不是仅仅等待函数的返回值。

异常对函数设计具有重要作用。传统的C函数设计不用异常。如果函数有多种结果,返

回类型只能有一个,就要添加形参来表示其它结果。添加的形参往往是指针类型,在调用时

要提供变量地址作为实参,在调用返回之后再判断得到什么结果。例如,一个求商函数可能

设计如下:

double quotient(int numrator, int denominator, int * isValid){

if (denominator == 0){

*isValid = 0; //0表示无效除数

return 0;

}

*isValid = 1; //1表示有效除数

return double(numrator) / denominator;

}

上面函数中商作为返回值,添加了一个引用形参来表示除数是否有效。另一种可行的C

函数设计是返回一个int值,0表示无效除数,1表示有效除数,而将商作为形参,如下所

示:

int quotient(int numrator, int denominator, double* result){

if (denominator == 0)

return 0;

*result = double(numrator) / denominator;

return 1;

}

可以看出,传统的C函数设计把除数为0作为一种特殊情形,用if语句加以判断处理,

需要更多的函数形参,导致函数定义复杂化。另一方面,调用方在调用函数返回之后,就要

立即用一个if语句来判断返回值是否为1,如为1,商才有效,如不为1,商无效。这对调

用方有一种强迫性,必须立即做出判断,这将导致程序逻辑复杂化,而且难以清晰表达正常

流程。

异常是C++提供的一种新概念,表示了偏离正常流程的小概率事件。异常不应该使正常

流程的描述复杂化,也不应该让调用方忽视可能发生的异常。调用方可以选择在适当的地方

集中捕获处理多种异常,就要用到try-catch语句。

使用throw语句,应注意以下要点:

(1)根据当前异常情形,应选择更准确、更具体的异常类型来引发,而避免引发抽象的类

型。例如,如果在new申请内存之后,如果发现返回空指针,此时应引发OutOfMemory

型的异常,而不是NullPointer异常,也不是更抽象的runtime_error或者exception。准确具

体的异常信息对于调用方的处理非常重要,否则就可能导致误解。

(2)如果一个函数中使用throw语句引发异常到函数之外,应该在函数原型中用异常规范

准确描述,即“throw(异常类型表),使调用方知道可能引发的异常类型,提醒调用方不要

忽视。

(3)虽然throw语句可以在函数中任何地方执行,但应尽可能避免在构造函数、析构函数

中使用throw语句,因为这将导致对象的构建和撤销过程中出现底层内存错误,可能会导致

程序在捕获到异常之前就被终止。后面15.8节将分析其原因。

(4)一般来说,异常发生总是有条件的,往往在一条if语句检测到某个假设条件不成立时,

才用throw语句引发异常,以阻止下面代码执行。在一个函数中无条件引发异常,只有一个

理由,就是不想让其它函数调用,例如,一些实体类的拷贝构造函数和赋值操作函数如果不

想被调用,就将这些函数设为私有,同时用一条throw语句避免本类其它函数执行。

(5)千万不要认为,只要我的编程中没有throw语句就不会引发异常,没有异常就是可靠

的。你可以暂时忽略异常,但当假设条件不满足,异常总会发生。当异常发生时你就不知道

在何处出现异常,也不知道什么原因导致异常,更不知道如何处理能使程序继续执行。

15.3.2 try-catch语句

捕获处理异常的语句是try-catch语句,一条try-catch语句由一个try子句(一条复合语句)

和多个catch子句组成。一个catch子句包括一个异常类型及变量和一个异常处理器(一条复

合语句)。语法格式如下:

try{

可能引发异常的语句序列; //受保护代码

}catch(异常类型1 异常变量1){

处理代码1 //异常处理器1

}catch(异常类型2 异常变量2){

处理代码2 //异常处理器2

}...

}catch(...){

处理代码; //异常处理器

}

其中,关键字try之后的一个复合语句称为try子句。这个复合语句中的代码被称为受

保护代码,包含多条语句。受保护代码描述正常的执行流程,但这些语句的执行却可能引发

异常。如果执行没有发生异常,try-catch语句就正常结束,开始执行其下面语句。如果引发

了某种类型的异常,就按catch子句顺序逐个匹配异常类型,捕获并处理该异常。如果异常

被捕获,而且处理过程中未引发新的异常,try-catch语句就正常结束。如果异常未被捕获,

该异常就被引发到外层作用域。图15.3表示了try-catch语句的组成结构。

try正正

正正正正正

正正1

catch

正正正正1

正正正正正1

正正2

...

正正n

catch

正正正正2

正正正正正2

...

正正

正正

15.3 try-catch语句的组成结构

一条语句执行引发异常,有以下3种可能的原因:

1 该语句是throw语句。

2 调用函数引发了异常。

3 表达式执行引发了未命名的异常,如整数除数为0、挂空访问等。

例如下面try-catch语句,调用了前面介绍的求商函数quotient

try{

result = quotient(n1, n2); //A

cout<< "The quotient is "< //B

//...

}catch(invalid_argument ex){ //C

cout<<"invalid_argument:"<<(); //D

}

A行调用quotient函数,如果没有引发异常,就执行B行,然后try-catch语句就执行完

毕。如果A行引发了某种异常,B行就不执行,从C行开始匹配异常类型,因为A行函数

调用可能引发的异常类型正式catch子句要捕获的异常类型invalid_argument故此该异常对

象就替代了ex形参,之后再执行后面的一个复合语句,D行调用异常对象ex的成员函数得

到错误信息,然后打印出来。try-catch语句执行完毕。无论是否发生异常,这个try-catch

句都能执行完毕,下面语句都能执行。

异常是按其类型进行捕获处理的。一个catch子句仅捕获一类异常。一个catch子句由

一个异常类型及变量和一个异常处理器(一条复合语句)构成。异常类型及变量指明要捕获的

异常的类型,以及接受异常对象的变量。例如catch(invalid_argument ex),要捕获的异常类

型为invalid_argument,如果真的捕获到该类异常,那么变量ex就持有这个异常对象,这个

对象就是前面用throw语句引发出来的。

有一种特殊的catch子句,就是catch(...),该子句能匹配任何类型的异常,包括未

命名的异常,不过异常对象或值不能被变量捕获,故此不能提供确切的错误信息。在多个

catch子句中,这种catch子句应该排在最后。

在执行try子句中的受保护代码时,如果引发一个异常,系统就到catch子句中寻找处

理该异常类型的入口。这种寻找过程称为异常类型匹配。按如下步骤进行:

1throw语句引发异常事件之后,系统依次检查catch子句以寻找相匹配的处理异

常事件入口。如果某个catch子句的异常类型说明与被引发出来的异常事件类型相一致,该

异常就被捕获,然后执行该子句的异常处理器代码。如果有多个catch子句的异常类型相匹

配,按照前后次序只执行第一个匹配的异常处理代码。因此较具体的派生类异常应该在匹配

在前,以提供最具体详细的信息,而较抽象的基类异常应该排在后面。

2)若没有找到任何相匹配的catch子句,该异常就被传递到外层作用域。如果外层

作用域是函数,就传递到函数的调用方。

一个异常的生命期从创建、初始化之后,被throw引发出来,然后被某个catch子句捕

获,其生命期就结束了。一个异常从引发出来到被捕获,可能穿越多层作用域或函数调用。

如果到main函数都未被捕获,将导致程序被迫终止。

从图15.3中可以看出,try-catch语句的执行结果有两个:正常和异常。表15.1分析了

try-catch语句的4种具体情形。

15.1 try-catch语句执行结果

序号 结果 具体情形

1 正常完毕 受保护代码未引发异常

2 正常完毕 受保护代码引发了异常,但异常被某个catch子句捕获

3 异常退出 受保护代码引发了异常,但未被catch子句捕获

4 异常退出 受保护代码引发了异常,而且被某个catch子句捕获,但在异常处理器中又

引发了新的异常,或者用“throw;”语句把刚捕获的异常又重新引发出来

分析下面try-catch语句的可能结果:

try{

result = quotient(n1, n2);

cout<< "The quotient is "<

//...

}catch(invalid_argument ex){

cout<<"invalid_argument:"<<();

}catch(logic_error ex){

cout<<"logic_error:"<<();

}catch(exception ex){

cout<<"exception:"<<();

}catch(...){

cout<<"some unexpected exception";

}

上面try子句中调用了可能引发异常的函数quotient。这个try语句包含了4catch

句,这4catch子句的次序是较具体的派生类放在前面,较抽象的基类放在后面。最后一

catch子句可匹配捕获任意类型的异常,但因得不到异常对象,故此不能提供更多信息。

在一次执行时,如果引发异常,只能有一个catch子句捕获处理该异常。由于最后一个catch

子句能捕获所有类型的异常,而且所有的异常处理器代码中都不会引发异常,因此该

try-catch语句的执行结果是表中第1种或者第2种情形。

对于try-catch语句的理解和应用,应注意以下几点。

(1)try子句中的代码,称为受保护代码,实际上是受到下面若干catch子句的保护,使

try子句代码可以放心去描述正常处理流程,而无需每执行一步都要用if语句来判断是否

发生异常情形。

(2)并非try子句都可能引发异常,也并非catch子句要捕获try子句所引发的所有异常,

当前函数只需捕获自己能处理的异常。

(3)多个catch子句之间,不允许基类异常在前、派生类在后,否则将出现语法警告,这

使得列在后面的派生类捕获不到异常,而排在前面的基类先捕获到了。

(4)try-catch语句仅适合处理异常,并不能将其作为正常流程控制。

15.3.3 例子

15-1 控制流程测试。

#include

void testExcept(int i){

try{

if (i == 1)

throw "catch me when i == 1"; //A

if (i == 2)

throw i; //B

if (i == 0){

int d = (i+1) / i; //C

cout< //D

}

cout<<"i="<

}catch(int i){ //E

cout<<"catch an int: "<

}catch(char * ex){ //F

cout<<"catch a string:"<

}catch(...){ //G

cout<<"catch an exception unknown";

}

cout<<"tfunction returnn"; //H

}

void main(){

testExcept(0); //C行引发异常,G行捕获

testExcept(1); //A行引发异常,F行捕获

testExcept(2); //B行引发异常,E行捕获

testExcept(3); //未引发异常

}

上面try-catch语句中的受保护代码中可能引发3种类型的异常。A行引发const char *

类型的异常,B行引发int异常,C行执行整数除法时,除数为0,将引发底层的未命名的

异常,故此D行不会执行。

上面try-catch语句包含3个异常处理器。E行捕获int异常,F行捕获char*异常,G

捕获底层未命名异常。注意,1个和第2个异常处理器的次序无所谓,但第3个异常处理

器必须排在最后,因为排在它后面将捕获不到任何异常。H行是try-catch语句后的一条语句,

如果try-catch语句正常退出,那么H语句将执行。如果受保护代码中引发的异常未被捕获,

或者异常处理代码中引发异常,H语句都不能执行。

执行程序,输出如下:

catch an exception unknown function return

catch a string:catch me when i == 1 function return

catch an int: 2 function return

i=3 function return

在主函数中测试了4种情形,3次调用都引发异常而且被捕获,最后一次调用未引发

异常。上面例子中的异常是直接用throw语句或者除数为0的表达式来引发的,异常也可能

是因调用函数而引发的。

15-2 测试向量vector的下标越界异常。标准模板库STL提供的向量vector

是支持元素随机访问的一种常用容器,它有两种随机访问形式:operator[]at(),后者可引

out_of_range异常。

#include

#include

using namespace std;

void main(){

try{

vector vec(4); //A

int i = 0;

for(i = 0; i < 4; i++) //B

vec[i] = i + 1;

for(i = 0; i <= 4; i++)

cout<//C no exception

cout<

for(i = 0; i <= 4; i++)

cout<<(i)<<" "; //D throw exception when i==4

cout<

}catch(out_of_range ex){

cout<<"out of range:"<<()<

}catch(...){

cout<<"unexpectedn";

}

}

执行程序,输出如下:

1 2 3 4 -33686019

1 2 3 4 out of range:invalid vector subscript

上面程序测试两种按下标随机访问元素的成员函数。A行先创建了一个向量,包含4

int元素。B行对这4个元素初始化。C行调用operator[]来访问元素,输出第1行,当下

标越界时,并没有引发任何异常,只是读取的vec[4]元素的值是随机值。D行调用at(int index)

来访问元素,输出第2行。当下标越界时,引发了out_of_range异常,而不会按非法下标读

取值。

15-3 除数为0的异常。在整数除法中,如果除数为0就引发底层未命名异常,因此

有必要在除法执行之前判断除数是否为0如果除数为0就引发一个命名的异常来通知调用

方。编程如下:

#include

#include

using namespace std;

double quotient(int numrator, int denominator) throw(invalid_argument){

if (denominator == 0)

throw invalid_argument("denominator is zero"); //A

return double(numrator) / denominator;

}

void main(){

int n1, n2;

double result;

cout<<"Enter two ints(end-of-file ^Z to end):";

while (cin>>n1>>n2){

try{

result = quotient(n1, n2);

cout<< "The quotient is "<

}catch(invalid_argument ex){ //B

cout<<"invalid_argument:"<<();

}catch(logic_error ex){ //C

cout<<"logic_error:"<<();

}catch(exception ex){ //D

cout<<"exception:"<<();

}catch(...){ //E

cout<<"some unexpected exception";

}

cout<<"nEnter two ints(end-of-file ^Z to end):";

}

}

执行程序,输出如下:

The quotient is 0.607143

Enter two ints(end-of-file ^Z to end):^Z

执行了3次求商函数,其中第2次除数为0,此时A行引发了invalid_argument类型的

异常,那么B行的catch子句就捕获了该类异常。做如下修改、再测试验证:

1 如果修改A行,将invalid_argument改变为logic_error,那么C行的catch子句就

捕获了该类异常。

2 如果再修改A行,改变为exception,那么D行的catch子句就捕获了该类异常。

3 如果删除BCD行的catch子句,那么E行的catch子句就捕获了该类异常。

4 如果删除所有的catch子句,那么异常将引发到main函数之外,由系统给出一个

提示如图15.4。这个对话框中不能给出有效错误信息,只能终止程序。

这个例子说明了因调用函数而引发异常的捕获。这个例子也说明了catch(...)可以捕获任

何类型的异常,但编程中往往采用另一种方法来处理未捕获的异常。

15.4 main函数未捕获的异常提示

15.4 终止处理器

如果引发的异常未能被捕获,程序将被迫终止。在程序终止之前,系统提供了终止处理

(terminate handler),提供一个机会来清理系统资源,然后再终止程序。在

中分别提供了terminate()函数,前者是老版本,作为全局函数,后者是新版本,

定义在std命名空间之中。

在发生下面情形之一时将自动执行terminate()函数:

1 引发异常最终未能捕获。

2 析构函数在系统堆栈释放时引发了异常。

3 在引发某个异常之后系统堆栈遭破坏。

缺省的terminate函数将调用abort函数,但abort函数不执行清理而简单终止程序,因

此常常需要自行定义一个函数,作为terminate函数调用的函数,这要先准备一个无参且无

返回的函数f,然后调用set_terminate(f),将函数f作为终止处理器。

15-4 terminate函数的例子。

#include

#include

using namespace std;

void term_func(){ //A

cout << "term_func() was called by terminate().n";

// ... cleanup tasks performed here

// If this function does not exit, abort is called.

exit(-1);

}

void main(){

int i = 10, j = 0, result;

set_terminate( term_func ); //B

try{

if( j == 0 )

throw "Divide by zero!"; //C

else

result = i/j;

}catch(int){

cout << "Caught an integer exception.n";

}

cout << "This should never print.n";

}

执行程序,输出如下:

term_func() was called by terminate().

A行定义了一个函数要作为终止处理器,B行调用set_terminate将此函数作为终止处理

器。在头文件中的相关定义如下:

typedef void (*terminate_handler)();//函数指针类型,终止处理器

terminate_handler set_terminate(terminate_handler ph) throw();

void terminate();

头一行说明了一种函数指针的类型名,第二行说明了一个函数set_terminate,将一个函

ph说明为新的终止处理器。最后一行是异常处理器函数,缺省将调用abort函数。

C行引发的异常类型为const char*,显然不能被下面的catch子句捕获,该异常将

导致程序终止,将执行terminate函数,因为B行设置了新的终止处理函数term_func,那么

新的函数得到执行。通常情况下,设置终止函数的目的是释放资源,然后调用exit函数来终

止程序。

标准C++还支持意外处理器unexpected handler,但VC++6版本并不支持。

15.5 扩展新的异常类型

虽然前面图15.2给出了一个异常类型架构,但经常需要扩展自己的异常类型。例如,

虽然out_of_range类能用于说明下标越界,但未说明发生异常的下标究竟值是什么。再如,

前面例子中除数为0的异常使用了invalid_argument类,而实际上除数为0可能有多种情形,

而不一定都作为函数实参,因此有必要自行定义除数为0的异常类。

15.4给出了一组扩展的异常类型。扩展异常类主要是以logic_errorruntime_error

为基类来定义派生类。下面构造了一个头文件exceptions.h,包含了一组常用的异常类。

#ifndef EXCEPTIONS

#define EXCEPTIONS

#include

exception

logic_errorruntime_error

out_of_range

OutOfMemory

NullPointerIOException

DevideByZero

Index_out_of_range

OpenFileExceptionReadFileFail

15.4 扩展异常类型

#include

using namespace std;

//下标越界,记录下标

class Index_out_of_range : public out_of_range{

const int index;

public:

Index_out_of_range(int index1, const string& what_arg)

:index(index1), out_of_range(what_arg){}

int getIndex()const {return index;}

};

//除数为0

class DivideByZero : public runtime_error{

public:

DivideByZero(const string& what_arg)

:runtime_error(what_arg){}

};

//空指针

class NullPointer : public runtime_error{

public:

NullPointer(const string& what_arg)

:runtime_error(what_arg){}

};

//无可用内存

class OutOfMemory : public runtime_error{

public:

OutOfMemory(const string& what_arg)

:runtime_error(what_arg){}

ReadFileFail(long pos, const string& what_arg)

:errPos(pos),IOException(what_arg) {}

const long getErrPos()const{return errPos;}

};

#endif

读者可自行扩展合适的派生类,以适合软件开发的具体需要。下面部分例子要使用这些

异常类型。

15.6 异常类型的应用

利用扩展的异常类型,就可对许多已有程序进行改进。例如前面矩阵类模板TMatrix

中,有一个公有成员函数elemAt如下:

template

T & TMatrix::elemAt(int r, int c){ //按下标访问元素

if (r < 0 || r >= row)

throw r; //行下标越界,引发int异常

if (c < 0 || c >= col)

throw c; //列下标越界,引发int异常

return dp[r][c];

}

原先是引发int类型异常,这容易与其它异常混淆。现在就可以使用更明确的

Index_out_of_range类型的异常。上面2throw语句就可以分别改为:

throw Index_out_of_range(r, "row index in elemAt(int, int)");

throw Index_out_of_range(c, "col index in elemAt(int, int)");

TMatrix模板中另一个公有成员函数operator()(int r, int c)调用了elemAt函数,所以

operator()(int r, int c)函数也会引发下标越界异常。这些函数都没有显式说明异

常规范:

throw(Index_out_of_range)

这是由于VC++6没有对异常规范进行语法检查(Java语言要求明确的异常规范,否则语

法编译出错)。不过规范的设计应该显式说明每个函数的异常规范,以提示调用方可能引发

哪些异常,避免遗忘捕获处理。

15-5设计一个函数从一个文本文件中读取多个浮点数,放入一个向量vector

中,显示各元素,给出元素的个数,并按升序排序。要读取的文本文件包含任意多的浮点数,

用分隔符分开,例如:

8.9 9.1 10.1 11.2

4.5 5.6 6.7 7.8

0.1 1.2 2.3 3.4

这个例子将演示多种异常类型的引发和处理。编程如下:

#include

#include

#include

#include "exceptions.h"

using namespace std;

void getVectorFromFile(char * filename, vector &vfs)

throw(NullPointer, OpenFileException, ReadFileFail){

if (filename == NULL)

throw NullPointer("filename is null");

::ifstream ifs(filename, ios::in|ios::nocreate);

if (!ifs){

string msg = "open file:";

msg += filename;

msg += " fail for read";

throw OpenFileException(msg);

}

float f;

while(!()){

ifs >> f;

if (()){

string msg = "read file fail:";

msg += filename;

throw ReadFileFail((), msg);

}

_back(f);

}

();

return;

}

void main(){

try{

char filename[200];

cout<<"input file name to read:";

cin>>filename;

vector vf;

getVectorFromFile(filename, vf);

cout<<"元素个数:"<<()<<":"<

for(int i = 0; i < (); i++)

cout<<(i)<< " ";

cout<

sort((), ());

cout<<"after sortedn";

for(i = 0; i < (); i++)

cout<<(i)<< " ";

cout<

}catch(NullPointer ex){

cout<<()<

}catch(OpenFileException ex){

cout<<()<

}catch(ReadFileFail ex){

cout<<()<<" at "<

}catch(out_of_range ex){

cout<<"index out "<<()<

}catch (exception ex){

cout<<()<

}catch (...){

cout<<"exception unknown"<

}

}

函数getVectorFromFile的第一个形参是一个文件名,第二个形参是vector引用作

为结果。该函数可能引发3种命名异常,用异常规范throw说明,以提示调用方。该函数直

接引发3种类型的异常:

1 检查形参指针是否为空,可能引发NullPointer异常;

2 打开文件,可能引发OpenFileException异常,保存了出错的文件名;

3 读浮点数,可能引发ReadFileFail异常,保存了文件读错的位置。

在函数getVectorFromFile执行过程中只要引发任何一种异常,就不能得到结果。

主函数中使用try-catch语句来完成计算并捕获处理各种异常。先输入一个文件名,并

说明一个vector变量,再调用函数getVectorFromFile来得到结果,下面就是显示、排

序、再显示。其中在调用at(i)函数时可能引发out_of_range异常。

执行程序,在第1行输入一个文件名,输出如下:

异常。例如:

NULL值来调用函数,看是否导致NullPointerException

输入错误的文件名是否会导致OpenFileException

把某个浮点数的字符该为字符,看是否导致ReadFileFail

try子句中的代码描述了正常执行的逻辑,而各种异常的捕获处理都用catch子句描述。

这样就能将正常流程与异常处理分割开,不仅提高了程序的可读性和可维护性,而且增强了

应对多种错误的能力,提高了编程可靠性。

15.7 函数设计中的异常处理

在函数设计中何时要用到异常?有以下3个原则:

1、遇到小概率事件,应考虑使用异常。一种小概率事件往往就是一种异常情形,例如,

函数的指针形参在调用时却得到了空指针实参。再例如,要输入一个浮点数,但实际输入错

误。小概率事件也意味着在可靠性要求不高的前提下可以推迟处理、甚至忽略。前面很多例

子都有这样一个前提,即小概率事件不会发生。反之,如果不是小概率事件,就不适合用异

常。例如,读文件到文件尾eof判断,就不适合将读到文件尾作为一种异常来处理,它不是

小概率事件,因为每一次读取都应判断是否到达文件尾。

2、遇到某种情形,根据当前信息不能确定应该如何处理,应考虑用异常来通知调用方

处理。例如,一个函数从文本文件中读浮点数序列,文件名由形参提供,假如按调用方提供

的文件名打开文件失败,应如何处理?此时合理的办法就是告诉调用方,这个文件名打开失

败了,由调用方来决定是换一个文件名,还是放弃。反之,对于某种情形,如果函数可以处

理而且不违背约定,那么这种情形就不适合作为异常。例如对于堆栈stack操作pop,只有

先判断堆栈不为空,才能弹出pop元素。堆栈为空这种情形不适合作为异常。

3、向调用方报告的某种结果的描述比较复杂,就应考虑使用异常。传统的C语言编程

常用不同的int值来表示各种错误。例如一个函数从文本文件中读浮点数序列,可以让该函

数返回一个int值,而且约定返回0表示正常,-1表示实参空指针,-2表示打开文件失败,

-3表示读取数据失败等。但返回-2时,还应告知打开失败的文件名。当返回-3时,不仅要

告知文件名,还应告知导致读取失败的具体位置,即第几个元素读取失败,这样才方便调用

方有效解决问题,此时就需要用异常来详细描述。

在一个函数设计中,要调用一个可能引发某种异常的函数时,有哪些处理方式?当前函

数有下面4种选择:

1 捕获该异常并进行处理,使自己的调用方不需要捕获处理该异常。

2 捕获该异常,在处理代码中转换为另一种异常,再引发出去,让调用方来捕获处理

新的异常。

3 捕获该异常,处理(可能是记录异常发生),再将捕获到的异常引发出去,让外层调

用方来捕获处理。在处理代码中用不带表达式的throw语句可以转发已捕获到的异

常。

4 不捕获该异常,让外层调用方来捕获处理。可能是没有try-catch语句,也可能有

try-catch但没有catch子句能匹配所发生的异常类型。

应采取何种处理方式取决于当前函数所承担的异常处理的责任。1种方式完全承担了

该种异常处理的责任,使外层调用方可以放心调用而无需关心会发生此类异常。4种方式

则完全不承担责任,调用方必须考虑如何处理间接引发的异常。2种和第3种方式介于两

者之间,承担了部分责任,能捕获处理异常,也能引发异常。

无论采用哪一种方式,函数的异常规范应告知调用方可能会引发哪些类型的异常。这应

该是函数约定的一个重要部分。

15-6 异常处理流程的例子。

#include

#include

using namespace std;

float getValue(int i){ //A

try{

if (i < 0)

throw "index is out of range"; //B throw char *

if (i == 0)

throw 3.14f; //C throw float

if (i == 1)

throw i; //D throw int

if (i == 2)

throw 2.718; //E throw double

cout<<"in try block, i = "<< i <

}catch(int index){ //F catch int

cout<<"catch int exception:"<

}catch(float f){

cout<<"catch float exception:"<

throw; //G throw float

}catch(char * msg){

cout<<"catch char* exception:"<

throw exception(msg); //H throw exception

}

cout<<"below try-catch, i = "<

return i+1;

}

void main(){

for(int i = -1; i <= 3; i++){

try{

float f = getValue(i);

cout<<"f = "<

}catch(float f){

cout<<"main:catch a float exception:"<

}catch(double d){

cout<<"main:catch a double exception:"<

}catch(exception ex){

cout<<"main:catch an exception:"<<()<

}catch(...){

cout<<"catch an exception unknown"<

}

}

}

执行程序,输出如下(行号是为了方便解释而加入的)

1 catch char* exception:index is out of range

2 main:catch an exception:index is out of range

3 catch float exception:3.14

4 main:catch a float exception:3.14

i == 1; 引发int异常,被捕获,没有再引发其它异常出来。

i == 2; 引发double异常,没有被捕获。

getValuethrow(exception, float,

double)

main函数中用i==-13来调用该函数,并用try-catch语句来捕获所有这些

异常。输出情况如下:

i == -1,输出前2行。

i == 0,输出第34行。

i == 1,输出第567行。

i == 2,输出第8行。

i == 3,无异常,输出第91011行。

能否在捕获异常之后再恢复重新执行?一般来说,在执行某条语句引发异常被捕获之

后,不能简单地再从该语句重新执行,这是由于出错的原因往往在于该语句之前所执行的语

句。因此仅仅对引发异常的语句重新执行,一般来说没有实际意义。如果真的要重新执行,

就需要正常的流程控制,而且仅适用于一些可以纠正的错误情形。例如,从键盘输入一个文

件名错误而导致打开文件失败,此时可以重新输入文件名再尝试打开。编程如下:

do{

bool retry = false; //缺省为不重复执行

try{

char filename[200];

cout<<"input file name to read:";

cin>>filename;

vector vf;

getVectorFromFile(filename, vf); //该函数可能引发异常

//...

}catch(OpenFileException ex){ //捕获异常

cout<<()< //给出出错信息

retry = true; //设置重复执行标记

}

}while (retry); //do-while来重复执行

//...

上面代码将一个try-catch语句封装到一个do-while语句之中,再设置一个bool

标记值来控制何时需要重复执行try子句。

在一个函数中当检测到异常,或者捕获到异常,往往采用替代的办法来处理。例如,当

读取一个浮点数失败时,就约定一个缺省值作为结果。当读取一个浮点数矩阵失败时,就约

定一个缺省矩阵作为结果。这种替代法使后面的代码能正常执行。

这种替代法具有一定普遍性,符合事务处理的一般规律。例如,总经理将一项任务交付

给一个部门经理来完成,部门经理再将该任务的一部分交付给员工A来完成,期望该员工

能正常完成。但如果员工A遇到不能解决的问题而告知部门经理,此时部门经理就可以选

择另换一名员工B来完成,或者自己来完成。无论那一种替代,总经理的任务总能完成,

而无需关心该任务是如何完成的。

15.8 异常可能导致内存泄露

在一个函数执行过程中,因引发一个异常而导致控制流转移到外层作用域。此时当前作

用域中可能已创建有若干局部对象,那么这些局部对象将被自动撤销,这些对象的析构函数

会被自动调用执行,而且是在该异常被捕获之前。

当函数执行中用new创建一个对象之后,在用delete撤销该对象之前引发了异常,而

且控制流离开了当前函数,此时动态对象就不能自动回收,造成内存泄漏。这是因为指向动

态对象的指针作为该函数的局部变量在栈中被清除了。因此在一个函数中,在执行throw

句之前,应该先用delete撤销先前创建的动态对象,以防止内存泄漏。但如果调用一个可能

引发异常的函数,要避免内存泄漏就比较困难了。

15-7 虽能捕获异常,但导致内存泄漏。

#include

#include

double getSqrt(double * d){

if (*d < 0)

throw *d;

return sqrt(*d);

}

void main(){

try{

double * d = new double(-3.4);

double d2 = getSqrt(d); //A 可能引发异常

delete d; //B 如果A引发异常,此行不执行

}catch(double d){

cout<<"d < 0:"< //C 捕获异常之后,也不能再回收

}

}

如果A行引发了异常,Bdelete语句就不能执行,造成内存泄漏,根本原因是控制流

离开了当前动态对象指针所在的作用域。

无论一个函数是正常返回,还是异常退出,其内部创建的动态对象都应清理干净,才能

避免内存泄漏。解决此问题的一种方法是,用一个函数来取代new运算符,将指向动态对

象的指针保存在一个特别设计的堆栈中(可以称之为清除栈clearup stack)。当前函数中所有

动态创建的对象的指针都保存在一块连续空间中。在函数执行中,如果发生异常而要退出当

前函数,就将此空间中所有未回收的动态对象都用delete回收,然后再将控制流转向异常捕

获。这样即便程序终止也能避免内存泄漏。在Symbian C++(一种智能手机开发平台)就使

用这样的清除栈clearup stack方法来防止内存泄漏,这是因为手机内存极其有限。

15.9 构造与析构中的异常

构造函数和析构函数执行中也可能引发异常。

如果在构造函数执行中引发了一个异常,此时构造函数没有执行完成,对象还没有构造

完成,因此对此对象不执行析构函数。如果在引发异常之前已构造完成了一部分基类子对象

或者成员对象,那么这些基类子对象或成员对象将执行析构函数,而未构造完成的对象就不

会执行析构函数。例如下面一个类:

class StudentScore{

int num;

string name;

float score;

public:

StudentScore(int num, string name, float score)

:num(num), name(name), score(score){}

//...

}

该类的构造函数没有能保证学号/编号num和分数score都为正数值,因为这些属性持

有负数值在现实中是十分荒唐的错误。另外,学生的姓名name如果少于2个汉字也是不正

常的。因此这个构造函数需要改进。

15-8 构造函数引发异常。

#include

#include

#include "exceptions.h"

using namespace std;

class StudentScore{

int num;

string name;

float score;

public:

StudentScore(int num, string name, float score)

throw(invalid_argument){

if (num < 0)

throw invalid_argument("num < 0");

else

this->num = num;

if (() < 4)

throw invalid_argument("name length < 4");

else

this->name = name;

if (score < 0)

throw invalid_argument("score < 0");

else

this->score = score;

}

~StudentScore(){

cout<<"destructor for num = "<

}

int getNum()const{return num;}

string getName()const{return name;}

float getScore()const{return score;}

void setScore(float newscore){score = newscore;}

};

ostream & operator<<(ostream &os, const StudentScore &ss){

os<<()<<"t"<

<<"t"<

return os;

}

void main(){

try{

StudentScore ss1(1, "张三",92.5);

StudentScore *ss2 = new StudentScore(2, "李四", 87);

if (ss2 == NULL)

throw OutOfMemory("when create num = 2");

StudentScore ss3(3, "王五",-2); //引发异常

cout<

delete ss2;

}catch(invalid_argument ex){

cout<<"invalid argument:"<<()<

应设为私有,而且不再引发异常。

如果在析构函数执行中引发了一个异常,当前对象就不能再被安全地访问了,即使捕获

了异常。在析构函数中引发异常可能导致底层错误而被迫终止程序,即使能捕获异常,也可

能造成内存泄露,因此建议不要用引发异常来阻止撤销对象。

在撤销某些对象时往往有特定条件,比如要撤销一个客户,该客户还有若干账户对象要

依赖于它,那么该客户对象就不能被随便撤销。往往建立一个静态成员函数来撤销对象,

检查是否满足特定条件,如果满足再调用析构函数。如果不满足条件,可以引发异常,也可

以不引发异常而返回约定的值,表示不同的结果。

在创建一个对象数组时如果引发异常,只对已构建完成的元素执行析构函数。

异常exception就是在程序运行过程中所发生的、难以预料的、不正常的事件,而

导致偏离正常流程。异常处理exception handling就是在运行时刻对异常进行检测、

捕获、提示、传递等过程。

C++提供了引发异常的语句throw和捕获处理异常的语句try-catch

在运行时刻用throw引发的每个异常都可以描述为一个对象或一个值。在编译时

刻,每一种异常都可以描述为一种类型,可能是自定义的类,也可能是简单的整数

或字符串。

利用类的继承性建立一个异常类型的架构,对所处理的各种错误情形进行抽象和归

类;方便扩展新的异常类型;在编程中方便选取正确的异常类型,也方便按类型来

捕获处理异常。

提供了一个比较现成的架构。类logic_error表示逻辑错

误,类runtime_error表示运行期错误,各自定义了一组派生类表示具体的异常。

引发异常要使用throw语句,其作用是中止当前控制流,转向该异常的捕获程序。

可能到外层作用域,也可能到函数的调用方。如果该异常一直到main函数都没有

被捕获,就要强制终止程序。

一条语句执行引发异常,有以下3种可能的情形:

本身就是throw语句。

调用函数引发了异常。

表达式执行引发了底层异常,如整数除数为0、挂空访问等。

捕获异常的语句是try-catch,由一个try子句(一条复合语句,称为受保护代码)

多个catch子句组成。一个catch子句包括一个异常类型及变量和一个异常处理器(

条复合语句)

如果受保护代码执行没有发生异常,try-catch语句就正常结束。

如果受保护代码引发了某种类型的异常,就按catch子句顺序逐个匹配异常类型,

捕获并处理该异常。

如果异常被捕获,而且处理过程中未引发新的异常,try-catch语句就正常结束。

如果异常未被捕获,该异常就被引发到try-catch语句的外层作用域。

异常是按其类型进行捕获处理的。一个catch子句仅捕获一类异常。注意,基类的

catch子句不能排在派生类之前。

有一种特殊的catch子句catch(...)该子句能匹配任何类型的异常,包括底层异常。

一个异常的生命期从创建、初始化开始,被throw引发出来,然后被某个catch

如果异常未能被捕获,程序将被迫终止。在终止之前,系统将自动调用函数

在函数设计中需要使用异常的情形:(1)处理小概率事件;(2)当前不能确定如何处

在引发异常之前,应注意先将动态创建的局部对象先回收,否则将导致内存泄露。

避免在构造函数和析构函数中直接或间接地引发异常。

句捕获,其生命期就结束了,从引发到捕获可能穿越多层作用域或函数调用。

terminate(),来清理系统资源,然后终止程序。该函数缺省调用abort()函数,但用

户可定义一个函数来取代。

理而要通知调用方;(3)要告知的情形描述比较复杂,往往需要一个对象。

1. 对于关键词throw,下面哪一种说法是错误的?

A 函数中throw语句用来引发异常到当前作用域之外。

B 函数原型中throw用来说明该函数可能引发哪些类型的异常。

C throw语句引发的异常,一定要用try-catch语句来捕获,否则语法错误。

D throw语句的语法形式与return语句一样。

2. 如果一条语句执行引发了异常,那么可能的原因不包括下面哪一种情形?

A 该语句是throw语句。

B 该语句中调用的函数引发了异常。

C 该语句中的表达式计算引发了未命名的异常。

D 该语句外层没有try-catch语句。

3. 对于一条try-catch语句,下面哪一种说法是错误的?

A try子句中的代码可以不引发任何异常。

B try子句中的代码所引发的异常,一定要被某个catch子句捕获,否则语法出错。

C 多个catch子句所捕获的异常,不能出现基类在前,而派生类在后的情形。

D catch(...)应该是最后一个catch子句。

4. 对于一条try-catch语句的执行结果,下面哪一种说法是错误的?

A try子句执行没有引发任何异常,try-catch下面的语句继续执行。

B try子句引发了一个异常,该异常被某个catch子句捕获并处理,try-catch下面的语句继续执行。

C try子句引发了一个异常,但没有被任何一个catch子句捕获,此时try-catch语句引发异常,下面

语句不能继续执行。

D catch子句中不能用throw语句引发异常。

5. 假设Index_out_of_rangeout_of_range的派生类,out_of_rangeexception的派生类,给出下面

程序运行结果。

#include

#include "exceptions.h"

double getValue(int index){

if (index < 0 || index > 9)

throw Index_out_of_range(index, "valid range is [0..9]");

return index;

}

void main(){

try{

double d = getValue(10);

cout<<"d="<

}catch(int index){

cout<<"index is out of range:"<

}catch(out_of_range ex){

cout<<"catch a out_of_range:"<<()<

}catch(exception ex){

cout<<"catch an exception:"<<()<

}catch(...){

cout<<"catch an exception unknown"<

}

}

6. 给出下面程序的执行结果,分析程序中有哪些语句不可能执行,分析前3catch子句改变次序是

否会影响执行结果。

#include

void expTest(int i){

try{

if (i == 1)

throw "catch me when i == 1";

if (i == 2)

throw i;

if (i == 3)

throw 3.14;

if (i == 0){

int d = (i+1) / i;

cout<

}

cout<<"i="<

}catch(double d){

cout<<"catch a double:"<

}catch(int i){

cout<<"catch an int: "<

}catch(char * ex){

cout<<"catch a string:"<

}catch(...){

cout<<"catch an exception unknown";

}

cout<<"tfunction returnn";

}

void main(){

expTest(0);

expTest(1);

expTest(2);

expTest(3);

expTest(4);

}

7. IOException类为基类,扩展一个派生类表示矩阵读写错误,记录出错的行和列。编程如

下: