10.异常处理
10.1异常处理概述
10.2抛出异常
10.3异常捕获
10.4构造函数、析构函数与异常处理
10.5异常匹配
10.6标准异常及层次结构层次结构。
C++具有强大的扩展能力,同时也大大增加了产生错误的可能性。在编程时,不能忽略异常处理。处理异常的方法多种多样。错误处理代码分布在整个系统代码中,在任何可能出错的地方都进行异常处理,阅读代码时可以直接看到异常处理的情况,但是引起的代码膨胀将不可避免地使程序阅读困难。
来看这样的一个问题,两个数的调和平均数的定义是:这两个数字倒数的平均值的倒数,因此表达式为:2.0*x*y/(x+y)。如果y是x的负值,则上述公式将出现除数为零的情况。对于这种问题,处理办法之一是,如果其中一个参数是另一个参数的负值,则调用abort()函数。abort()函数的原型位于头文件cstdlib(或stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。
abort()函数实例:
#include
#include
using namespace std;
double hmean(double a, double b);
int main()
{
double x, y, z;
cout << "Enter two numbers:";
while(cin >> x >> y)
{
z = hmean(x, y);
cout << "Harmonic mean of" << x << " and " << y << " is " << z <:";
}
cout << "End!\n";
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
{
cout << "untenable arguments to hmean()\n";
abort();
}
return 2.0*a*b/(a+b);
}
注意,在hmean()中调用abort()函数将直接终止程序,而不是先返回到main()。一般而言,显示的程序异常中断消息随编译器而异。为了避免异常终止,程序应该在调用hmean()函数之前检查x和y的值。
一种比异常终止更灵活的方法是,使用函数返回值来指出问题。例如,ostream类的get(void)成员通常返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF。对hmean()来说,这种方法不管用。任何数值都是有效的返回值,因此不存在可用于指出问题的特殊值。在这种情况下,可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。istream族重载>>运算符使用了这种技术的变体。通过告知调用程序是成功还是失败,使得程序可以采取除异常终止程序之外的其他措施。
程序实例:
#include
#include
using namespace std;
bool hmean(double a, double b, double *ans);
int main()
{
double x, y, z;
cout << "Enter two numbers:";
while (cin >> x >> y)
{
if(hmean(x, y, &z))
{
cout << "Harmonic mean of " << x << " and " << y << " is " << z <: ";
}
cout << "End!\n";
return 0;
}
bool hmean(double a, double b, double *ans)
{
if(a == -b)
{
*ans = DBL_MAX;
return false;
}
else
{
*ans = 2.0*a*b/(a+b);
return true;
}
}
上例将hmean()的返回值重定义为bool,让返回值告诉我们是成功还是失败,第三个参数是用于提供答案(对内置类型的参数,用指针可以明显看出是哪个参数用于提供答案)。
C++的异常处理是一种允许两个独立开发的程序组件在程序执行期间遇到程序异常时,相互通信的机制。具有以下特点:
(1)异常处理程序的编写不再繁琐。在错误有可能出现处写一些代码,并在后面的单独节中加入异常处理程序。如果程序中多次调用一个函数,在程序中加入一个函数异常处理程序即可。
(2)异常发生不会被忽略。如果被调用函数需要发送一条异常处理信息给调用函数,它可向调用函数发送一描述异常处理信息的对象。如果调用函数没有捕捉和处理该错误信号,在后续时刻该调用函数将继续发送描述异常信息的对象,直到异常信息被捕捉和处理为止。
异常处理被用来处理同步错误,如除数为0、数组下标越界、运算溢出和无效函数参数等。异常处理通常用于发现错误的部分与处理错误的部分不在同一范围时。与用户进行交互式对话的程序不能用异常处理来处理输入错误。异常处理特别适合用于程序无法恢复但又需要提供有序清理,使得程序可以正常结束的情况。异常处理不仅提供了程序的容错性,还提供了各种捕获异常的方法,如根据类型捕获异常,或者指定捕获任何类型的异常。
如果程序发生异常情况,而在当前的上下文环境中获取异常处理的足够信息,可以创建一个包含出错信息的对象并将此对象抛出当前的上下文环境,将出错信息发送到更大的上下文环境中,称为异常抛出。
抛出异常语法如下:
throw ourerror("some error happened");
其中ourerror是一个普通的自定义类。如有异常抛出,可以使用任意类型变量作为参数。一般情况下,为清晰地区分异常信息,创建一个新类用于异常抛出。当异常发生时,通过throw调用构造函数创建一个自定义的对象ourerr,此对象正是throw函数的返回值,通常这个对象不是函数设计的正常返回值类型;且异常抛出的返回点不同于正常函数调用返回点。例如,在函数f()中抛出异常:
void f()
{
throw int(5);
}
可以根据要求抛出不同类型的异常对象。为能清晰地区分不同类型异常,可根据错误类型设计不同类型的对象。
10.3.1 异常处理语法
如果函数内抛出一个异常(或在调用函数时抛出一个异常),则在异常抛出时系统会自动退出所在函数的执行。如不想在异常抛出时退出函数,可在函数内创建一个特殊块用于测试各种错误。测试块作为普通作用域,由关键字try引导,异常抛出后,由catch引导的异常处理模块应能接受任何类型的异常。在try之后,根据异常的不同情况,相应的处理方法由关键字catch引导。语法如下:
try{
//可能发生错误的代码
}catch(type1 t1){
//第一种类型异常处理
}catch(type2 t2){
//第二种类型异常处理
}
......//其他类型异常处理
异常处理部分必须直接放在测试块之后。每一个catch语句相当于以特殊类型为参数的函数(如类型type1、type2等)。如果异常抛出给出的异常类型足以判断如何进行异常处理,则异常处理器catch中的参数可以省略。
对try部分抛出的异常,系统将从前到后逐个与catch后所给出的异常类型相匹配。如果匹配成功,则进入相应的处理部分执行异常处理程序。
异常处理的执行流程如下:
(1)程序进入到try块,执行try块内的代码。
(2)如果在try块内没有发生异常,则直接转到所有catch块后的第一条语句执行下去。
(3)如果发生异常,则根据throw抛出的异常对象类型来匹配一个catch语句(此catch能处理此种类型的异常,即catch后的参数类型与throw抛出异常对象类型一致)。如果找到类型匹配的catch语句,进行捕获,其参数被初始化为指向异常对象,执行相应catch内的语句模块;如果找不到匹配类型的catch语句,系统函数terminate被调用,终止程序。
需要注意的是,通常在try块中,可能有已经分配但尚未释放的资源,如果可能,catch处理程序应该释放这些资源。例如,catch处理程序删除通过new分配的空间,关闭抛出异常的try块中打开的文件。对于catch块来说,处理错误之后可以让程序继续运行,也可以终止程序。
#include
using namespace std;
class Divdebyzero
{
const char* message;
public:
Divdebyzero():message("divided by zero"){}
const char* what(){return message;}
};
double testdiv(int num1, int num2)
{
if(num2 == 0)
{
throw Divdebyzero();
}
return (double)num1/num2;
}
int main()
{
int num1,num2;
double res;
cout << "please input two integers:";
while(cin >> num1 >> num2)
{
try{
res = testdiv(num1, num2);
cout << "the res is :" << res <
10.3.2异常接口声明
C++提供了异常接口声明语法,利用它可以清晰的告诉使用者异常抛出的类型。异常接口声明再次使用了关键字throw,语法如下:
void f() throw(A,B,C,D); //此函数只能抛出A,B,C,D及其子类型异常。
传统函数:void f();意味着能抛出任何一种异常。
void f() throw();表明函数不会有异常抛出。
10.3.3捕获所有异常
C++中可以声明:
catch(......){}来捕获所有类型的异常。
在一些情况下,可能无法处理有关异常信息,可以通过不加参数的throw来重新抛出异常,使得异常进入更高层次的上下文环境。语法如下:
catch(.....){
cout << "一个异常被抛出!";
throw;
}
throw只能出现在catch子句的复合语句中,且被抛出的异常就是原来的异常对象。
10.3.4未捕获异常的处理
如果任意层的异常处理器都没有捕获到异常(没有指定相应的catch块),称为”未捕获异常”。系统的特殊函数terminate()将自动调用,该函数通过调用abort()函数来终止程序的执行。
#include
using namespace std;
class Bummer{};
class Killer{};
void foo()
{
int error = 1;
if(error)
{
cout << "throwing Killer" <
在主函数调用函数foo()是抛出异常类型Killer,而主函数中的异常处理块catch只能处理Bummer类型的异常,所以发生了未捕获异常的情况,通过调用系统的特殊函数terminate()终止程序。
异常处理部分的常见错误在于异常抛出时,对象没有被正确清除。虽然C++的异常处理器可以保证当离开一个作用域时,该作用域中所有结构完整的对象的析构函数都能被调用,以清除这些对象,但是当对象的构造函数不完整时其析构函数将不被调用。
构造函数中发生异常后,异常处理遵从以下规则:
(1)如果对象有成员函数,且如果在外层对象构造完成之前有异常抛出,则在发生异常之前,执行构造成员对象的析构函数。
(2)如果异常发生时,对象数组被部分构造,则只调用已构造的数组元素的析构函数。
(3)异常可以跳过通常释放资源的代码,从而造成资源泄漏。解决办法是,请求资源时初始化一个局部对象,发生异常时,调用析构函数并释放资源。
(4)要捕捉析构函数中的异常,可以将调用析构函数的函数放入try块,并提供相应类型的catch处理程序块。抛出对象的析构函数在异常处理程序执行完毕后执行。
从基类可以派生各种异常类,当一个异常抛出时,异常处理器会根据异常处理顺序找到”最近”的异常类型进行处理。如果catch捕获了一个指向基类型异常对象的指针或引用,那么它也可以捕获该基类所派生的异常对象的指针或引用。相关错误的多态处理是允许的。
#include
using namespace std;
class Basicerr{};
class Childerr1:public Basicerr{};
class Childerr2:public Basicerr{};
class Test
{
public:
void f(){throw Childerr2();}
};
int main()
{
Test t;
try
{
t.f();
}catch(Basicerr)
{
cout << "catching Basicerr" <
对于这里的异常处理机制,第一个处理器总是匹配一个Basicerr对象或从Basicerr派生的子类对象,所以第一个异常处理捕获第二个或第三个异常处理的所有异常,而第二个和第三个异常处理器永远不被调用。因此在捕获异常中,常把捕获基类类型的异常处理器放在最末端。
C++标准提供了标准库异常及层次结构。标准异常以基类exception开头(在头文件
由基类exception直接派生的类runtime_error和logic_error(均定义在头文件