异常说明
程序有时候会在运行阶段出现错误,导致程序无法正常继续运行下去,例如,程序会试图打开应该不可用的文件,请求过大的内存等等,通常,我们都需要预防这种情况的发生,C++异常为处理这种情况提供了一种功能强大且灵活的工具,异常是相对较新的C++功能,可能有些老的编译器会不支持这种功能
我们通过一个例子来看看一些不使用异常的方法,例如我们编写一个计算两个数的调和平均数,两个数的调和平均数是两个数倒数的平均值的倒数,因此表达式为2.0 * x*y / (x + y),因此
当我们传递的x与y是相反数时,会导致(x+y)的值为0,这样就是除数为0的情况,这是一种不被允许的运算,对于这种情况,很多编译器都会生成一个表示无穷大的特殊浮点数来进行处理,cout会显示为Inf,inf,INF等类似的东西
对于这种结果,编译器并没有给我们爆出错误,只是告诉我们这个值是无穷大,而我们希望编译器能够给我们提示一些错误,并且有对应的处理机制,对于这种问题,我们处理的方式之一是,如果其中一个参数是另外一个参数的负值,则调用abort()函数
abort()函数
abort()函数的原型位于头文件stdlib.h中,它实现是向标准错误流(cerr使用的错误流)中发生程序异常终止信息,然后终止程序,并返回一个随现实而异的值,告诉操作系统(如果程序是由另一个程序调用,则告诉父进程)处理失败,abort()函数是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现,如果愿意,也可以调用exit()函数,该函数会刷新文件缓冲区,但是不会显示信息,下面是一个使用abort()函数的例子
如果我们输入的参数不符合要求,程序会出现错误,我们也可以在程序终止之前自己给出提示,提示我们输入的参数不符合,但在这个代码中没有提供提示,程序运行的结果如下
返回错误码
对于我们使用abort()函数来终止异常的方法并不是最好的,我们不希望程序一遇到错误就终止,而是希望能够让用户重新输入,或者忽略掉错误情况,有错误修正的机制,这样我们的程序才会更健壮,而不是程序一出错就停止,因此,我们可以使用返回错误码的方式来指出我们的问题
例如ostream类的get(void)成员通常在返回下一个输入字符的ASCII码,但达到末尾时,会返回EOF,但是对于我们的程序来说,这种方法并没有用,因为当我们将两个数的求和平均数作为一个函数时,函数的返回值都是有效的,因此就没有可以指出问题的特殊值,在这种情况下,我们就可以使用指针参数或者引用参数将值返回给调用程序,并告知函数的返回值是成功还是失败,因此,我们就可以改写上面的程序
#include<iostream>
#include<cstdlib>
using namespace std;
bool hmean(double x, double y, double *ans)
{
if(x == -y)
{
return false;
}
else
{
*ans = 2.0 * x * y /(x + y);
return true;
}
}
int main()
{
double x , y, result;
cout<<"Enter to numbers : ";
while(cin>>x>>y)
{
if(hmean(x,y,&result))
cout<<"Harmonic mean of "<<x<<" and "<<y<<" is "<<result<<endl;
else
cout<<"One of the values should not be the opposite of the other\n";
cout<<"Enter next set of numbers: ";
}
return 0;
}
程序的运行结果如下:
我们根据返回值的类型来进行判断,如果输入数据不符合要求就给出错误提示信息,并且能够重新输入,这样就防止了我们在出现错误信息时导致程序终止
异常机制
下面介绍一下异常机制来处理错误,C++异常是对程序运行过程中发生异常情况(如被0除)的一种相应,异常提供了将控制权从程序的一部分传递到另一个部分的途径,对异常的处理由下面的三个部分组成
1.引发异常
2.使用处理程序捕获异常
3.使用try块
1.引发异常
程序在出现问题时会引发异常,thorw关键字表示引发异常,紧随其后的值(可以是字符串或者对象)指出的异常的特征,它实际上是跳转,即命令程序跳转到另外一条语句,我们可以改写我们的例子,看看引发异常的结果,程序如下
在这里,我们被引发的异常是字符串,异常类型可以是字符串类型或者C++其他类型,通常是类类型,我们执行throw语句类似执行返回语句,因为它也会终止函数(hmean函数)的执行,但是throw不会将控制权返回给调用程序,而是导致程序沿着函数调用序列后退,直到找到try块的函数,在我们的这个例子中,main函数调用hmean函数,因此throw会将程序的控制权返回给main函数,并在main函数中寻找与引发异常类型匹配的异常处理程序(位于try块的后面)
2.捕获异常
程序通常会使用异常处理程序来捕获异常,异常处理程序通常在要处理问题的程序中,catch关键字表示捕获异常,处理程序会有关键字catch开头,随后就是位于括号中的类型声明,它指出了异常处理程序要相应的异常类型,然后就是一个使用花括号括起来的代码块,指出要采取的措施,关键字catch和异常处理类型用作标签,指出当异常被引发时,程序应该跳转到标签的位置进行执行,异常处理程序也被称为catch块
3.使用try块
try块标识其中特定的异常可能被激活的代码块,它的后面需要跟随一个或者多个catch块,try块是由关键字try指示的,关键字try后是一个花括号括起来的代码块,表明需要注意引发的异常
异常实例
我们可以通过一个例子看看这三个代码是怎么协同工作的,代码如下
#include<iostream>
#include<cstdlib>
using namespace std;
double hmean(double a, double b);
int main(void)
{
double x, y, z;
cout << "Enter two numbers: ";
while(cin >> x >> y)
{
try
{
z = hmean(x, y);
}
catch(const char *s)
{
cout << s << endl;
cout << "Enter a new pair of arguments:";
continue;
}
cout << "Harmonic mean of " << x << " and " << y << " is " << z << endl;
cout << "Enter next set of number: ";
}
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
throw "bad hmean() arguments, a = -b not allowed";
return 2.0 * a * b / (a + b);
}
运行结果:
对于我们这个例子,当输入错误时,在函数hmean中if语句引发了hmean的异常,这样就会终止函数hmean的执行,程序向后搜索发现hmean函数是main函数调用的,因此程序会查找到与异常匹配的catch块,程序中唯一一个catch块的参数是const char *类型,因此与它发生匹配,因此将字符串"bad hmean() arguments, a = -b not allowed"赋值给s,然后执行catch块中的内容,如果程序是在try块的外面调用hmean,则会导致无法处理异常
将类的对象作为异常类型
通常引发异常的函数将会传递一个对象,这样做能够使用不同的异常类型来区分不同的函数在不同情况下引发的异常,并且对象能够携带信息,我们可以根据这些信息来确定引发异常的原因,同时,catch块能够根据这些信息来决定采取什么措施,我们可以通过一个例子来看看如何实现将类的对象作为异常
#include<iostream>
#include <cstdlib>
#include <cmath>
using namespace std;
//调和平均数
class bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(double a = 0, double b = 0):v1(a),v2(b){}
void mesg()
{
cout<<"hmean ( "<<v1<<" , "<<v2<<" ) : invalid argument : a = -b\n";
}
};
//调和平方根
class bad_gmean
{
private:
double v1;
double v2;
public:
bad_gmean(double a = 0, double b = 0):v1(a),v2(b){}
const char *mesg()
{
return "gmean() arguments should be +\n";
}
double getValue1() const { return v1; }
double getValue2() const { return v2; }
};
double hmean(double a, double b);
double gmean(double a, double b);
int main(void)
{
double x, y, z;
cout << "Enter two numbers: ";
while(cin >> x >> y)
{
try
{
z = hmean(x, y);
cout << "Harmonic mean of " << x << " and " << y << " is " << z << endl;
z = gmean(x, y);
cout << "Geometric mean of " << x << " and " << y << " is " << z << endl;
}
catch(bad_hmean &bh)
{
bh.mesg();
cout << "Enter a new pair of arguments:";
continue;
}
catch(bad_gmean &bg)
{
cout << bg.mesg();
cout << "Value used: " << bg.getValue1() << ", " << bg.getValue2() << endl;
cout << "Sorry, quit now" << endl;
break;
}
cout << "Enter next set of number: ";
}
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
throw bad_hmean(a, b);
return 2.0 * a * b / (a + b);
}
double gmean(double a, double b)
{
if(a < 0 || b < 0)
throw bad_gmean(a, b);
return sqrt(a*b);
}
程序运行结果如下:
根据这个运行结果,我们就可以知道是程序的哪个部分发生了错误
栈解退
我们上面的程序中,try块都是直接调用了引发异常的函数
try
{
z = hmean(x, y);
cout << "Harmonic mean of " << x << " and " << y << " is " << z << endl;
z = gmean(x, y);
cout << "Geometric mean of " << x << " and " << y << " is " << z << endl;
}
假设我们的try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,即try块调用的函数不会引发异常,但在这函数中再去调用其他的函数可能引发异常,则程序流程将会从引发异常的函数跳转到包含try块和处理程序的函数,这就涉及到了栈解退
C++通常通过将信息存储到栈中来处理函数调用,即程序将调用函数的指令的地址存放到栈中,当被调用的函数执行完毕之后,程序会使用该地址来确定从哪个地方开始继续执行,此外,函数调用也会将函数的参数放入栈中,在栈中,这些参数被看做是自动变量,如果被调用的函数创建了新的变量,那么这些变量也会存入栈中,如果被调用的函数调用了其他的函数,则后者的信息也会被存储到栈中,依次类推,当函数调用结束时,程序流程会跳转到该函数被调用时存储的地址处,同时栈顶的元素被释放,因此,函数通常都会返回调用它的函数,依此类推,同时每个函数都会在结束时释放其变量,如果自动变量是类对象,也会自动去调用去析构函数,即如下图所示
假设函数由于出现异常而被终止,则程序会继续释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址,随后,控制权会转移到块尾的异常处理程序,而不是函数调用的第一条语句,这个就是栈解退的过程,如下图所示
在main函数中,调用函数a,函数a去调用函数b,而函数b又去调用函数c,当函数c引发异常时,c函数会被终止,并且程序会从引发异常的函数c,一直返回,直到跳转到try块和处理程序的函数,我们可以通过改写上面的代码来看看这个栈解退的过程,我们在原有的代码上增加了一个函数mean,它是一个不会引发异常的函数,通过这个函数去调用另外了两个会引发异常的函数
#include <iostream>
#include <cstdlib>
#include <cmath>
using namespace std;
class bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(double a = 0, double b = 0) : v1(a), v2(b){}
void mesg();
};
inline void bad_hmean::mesg()
{
cout << "hmean(" << v1 << ", " << v2 << "): invalid arguments: a = -b" << endl;
}
class bad_gmean
{
public:
double v1;
double v2;
bad_gmean(double a = 0, double b = 0) : v1(a), v2(b){}
const char *mesg();
};
inline const char *bad_gmean::mesg()
{
return "gmean() arguments shoud be >= 0";
}
double hmean(double a, double b);
double gmean(double a, double b);
double mean(double a, double b);
int main(void)
{
double x, y, z;
cout << "Enter two numbers: ";
while(cin >> x >> y)
{
try
{
z = mean(x, y);
cout << "The mean mean of " << x << " and " << y << " is " << z << endl;
}
catch(bad_hmean &bh)
{
bh.mesg();//cout << s << endl;
cout << "2) exception" << endl;
cout << "Enter a new pair of arguments:";
continue;
}
catch(bad_gmean &bg)
{
cout << bg.mesg();
cout << "Value used: " << bg.v1 << ", " << bg.v2 << endl;
cout << "Sorry, quit now" << endl;
break;
}
cout << "Enter next set of number: ";
}
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
throw bad_hmean(a, b);
return 2.0 * a * b / (a + b);
}
double gmean(double a, double b)
{
if(a < 0 || b < 0)
throw bad_gmean(a, b);
return sqrt(a*b);
}
double mean(double a, double b)
{
double am, hm, gm;
am = (a + b) / 2.0;
try
{
hm = hmean(a, b);
gm = gmean(a, b);
}
catch(bad_hmean &bh)
{
bh.mesg();
cout << "1) expception" << endl;
throw;
}
return (am + hm + gm) / 3.0;
}
运行结果如下:
当我们运行程序,第一次在main函数中去调用mean函数时,程序就会跳转到mean函数处,之后在mean函数中调用hmean函数,到达hmean函数中引发了bad_heam类的异常,之后这个异常就被mean函数中的catch块捕获,因为引发异常的类型和处理异常的类型相同,因此就会执行mean函数中catch的内容,catch执行的最后,又抛出了一个异常,异常就会返回到main函数中,在main函数中又被处理相同类型的catch捕获并且继续处理,当处理完成之后,我们重新输入两个负数,程序重新运行到mean函数,并且调用mean函数中的gmean,这就会导致gmean函数发生异常,此时又会程序又会从gman函数回退到mean函数,因为在mean函数中没有发现处理合适类型的异常处理程序,因此又会回退到main函数中,并在这时候发现了类型匹配的异常处理程序,进行异常处理,执行完这个异常处理之后有一个break,因此就会跳出循环,不会再让我们输入,这就是栈解退的过程
其他的异常特性
函数引发异常时的返回机制与正常函数的返回机制不同,并且在引发异常时,编译器总是会创建一个临时的拷贝,即使异常规范和catch块中指定的是引用,我们使用引用,是因为基类引用可以指向派生类的对象,假如有一组通过继承关联起来的异常类型,则在异常规范中只需要列出一个基类引用即可,它将与任何派生类对象匹配
注意,当我们使用派生类抛出异常时,异常处理程序的先后顺序应该是按照派生类到基类的顺序进行匹配,这意味着先匹配派生类的异常处理程序,然后再匹配基类的异常处理程序,如果没有匹配的派生类异常处理程序,异常会继续向上匹配基类的异常处理程序,直到找到匹配的处理程序或者到达 catch
块的末尾,这种异常处理程序的搜索顺序确保了派生类的异常可以被专门处理,同时也可以提供通用的基类异常处理,如果有一个异常处理程序能够处理所有的异常,则可以将它放在最后
exception类
C++异常的主要目的是为了设计容错程序提供语言支持,即异常使得在程序设计中包含错误处理功能更加容易,以免事后采取一些严格的错误处理方式
在较新的C++编译器将异常合并到语言中,例如为了支持该语言,在exception头文件中定义了exception类,C++可以将其作为其他异常类的基类,代码可以引发exception异常,也可以将exception类用作基类,有一个名为what()的虚成员函数,它返回一个字符串,该字符串的特征随着实现而不同,因此可以在由exception派生的类中重新定义它,如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序捕获它们,因为基类的指针可以指向派生类
在C++库中定义了很多基于exception的异常类型,例如stdexcept异常类,头文件stdexcept定义了其他几个异常类,该头文件定义了logic_error类和runtime_error类,它们都是以公有的方式从exception类中派生而来的
异常,类和继承
异常,类和继承三种方式相互关联,我们可以从一个异常类派生出另外一个异常类,并且可以在类定义中嵌套异常类声明来组合异常,这种嵌套声明本身可以被继承,还可以用作基类,我们可以通过一个例子来看看它们三者的的关系
在头文件中的Sales类,用于存储一个年份以及一个包含12个月的销售数据的数组,另一个类是从Sales类中派生而来,新增加了一个用来存储数据标签的成员
#ifndef __SALES_H__ #define __SALES_H__ #include <iostream> #include <stdexcept> #include <string> using namespace std; class Sales { public: enum{MONTHS = 12}; explicit Sales(int yy = 0); Sales(int yy, const double *gr, int n); virtual ~Sales(){}; int Year() const{return year;} virtual double operator[](int i) const; virtual double &operator[](int i); class bad_index : public logic_error { private: int bi; public: explicit bad_index(int ix, const string &s = "Index error in Sales object\n") : bi(ix), logic_error(s){} int bi_val() const{return bi;} //代表析构函数不会引发任何异常 virtual ~bad_index() throw(){} }; private: int year; double gross[MONTHS]; }; class LabeledSales : public Sales { private: string label; public: explicit LabeledSales(const string &lb = "none", int yy = 0) : Sales(yy), label(lb){} LabeledSales(const string &lb, int yy, const double *gr, int n) : Sales(yy, gr, n), label(lb){} virtual ~LabeledSales(){} const string &Label() const{return label;} virtual double operator[](int i) const; virtual double &operator[](int i); class nbad_index : public Sales::bad_index { private: string lbl; public: nbad_index(const string &lb, int ix, const string &s = "Index error in LabeledSales object\n") : Sales::bad_index(ix, s), lbl(lb){} const string &label_val() const{return lbl;} virtual ~nbad_index() throw(){} }; }; #endif
函数定义文件
#include "sales.h" Sales::Sales(int yy) { year = yy; for(int i = 0; i < MONTHS; i++) gross[i] = 0; } Sales::Sales(int yy, const double *gr, int n) { year = yy; int lim = n < MONTHS ? n : MONTHS; int i; for(i = 0; i < n; i++) gross[i] = gr[i]; for(; i < MONTHS; i++) gross[i] = 0; } double Sales::operator[](int i) const { if(i < 0 || i >= MONTHS) throw bad_index(i); return gross[i]; } double &Sales::operator[](int i) { if(i < 0 || i >= MONTHS) throw bad_index(i); return gross[i]; } double LabeledSales::operator[](int i) const { if(i < 0 || i >= MONTHS) throw nbad_index(Label(), i); return Sales::operator[](i); } double &LabeledSales::operator[](int i) { if(i < 0 || i >= MONTHS) throw nbad_index(Label(), i); return Sales::operator[](i); }
测试文件
#include <iostream> #include "sales.h" using namespace std; int main(void) { double vals1[12] = {1100, 1110, 1120, 1130, 1140, 1150, 1160, 1170, 1180, 1190, 1200, 1210}; double vals2[12] = {11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}; Sales sales1(2011, vals1, 12); LabeledSales sales2("ABC", 2012, vals2, 12); try { cout << "Year = " << sales1.Year() << endl; for(int i = 0; i < 12; i++) { cout << sales1[i] << ' '; if(i % 6 == 5) cout << endl; } cout << "Year = " << sales2.Year() << endl; cout << "Label = " << sales2.Label() << endl; for(int i = 0; i <= 12; i++) { cout << sales2[i] << ' '; if(i % 6 == 5) cout << endl; } } catch(LabeledSales::nbad_index &bad) { cout << bad.what(); cout << "Company: " << bad.label_val() << endl; cout << "bad index: " << bad.bi_val() << endl; } catch(Sales::bad_index &bad) { cout << bad.what(); cout << "bad index: " << bad.bi_val() << endl; } cout << "----------------------------" << endl; try { sales2[2] = 40; sales1[20] = 3333; } catch(LabeledSales::nbad_index &bad) { cout << bad.what(); cout << "Company: " << bad.label_val() << endl; cout << "bad index: " << bad.bi_val() << endl; } catch(Sales::bad_index &bad) { cout << bad.what(); cout << "bad index: " << bad.bi_val() << endl; } return 0; }
引发异常的类与捕获异常的类的顺序是相反的