异常,类和继承
一、异常,类和继承
- 异常,类和继承以三种方式相互关联。
- 首先,可以像标准c++库所做的那样,从一个异常类派生出另一个;
- 可以在类定义中嵌套异常类声明来组合异常,这种嵌套声明本身可被继承,还可用作基类。
实例:
//声明一个Sales类,它用于存储一个年份以及一个包含12个月的数据的数组。
//LabeledSales类时从Sales派生而来的,新增了一个用于存储数据标签的成员。
#ifndef SALES_H
#define SALES_H
#include<stdexcept>
#include<string>
using namespace std;
class Sales
{
private:
double gross[MONTHS];//派生类能使用
int year;
public:
enum{MONTHS=12};//可能是静态常量
class bad_index:public logic_error//客户类的catch可以使用这个类作为类型
{
private:
int bi;
public:
explicit bad_index(int ix,const string & s ="Index error in Sales object\n");
int bi_val()const{return bi;}
virtual ~bad_index()throw(){}
};
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 LabeledSalese:public Sales
{
public:
class nbad_index:public Sales::bad_index//nbad_index从logic_error派生而来
{
private:
string lbl;
public:
nbad_index(const string &lb,int ix,
const string &s="Index error in LabeledSales object\n");
const string & label_val()const{return lbl;}
virtual ~nbad_index()throw(){}
};
explicit LabeledSalese(const string & lb="none",int yy=0);
LabeledSalese(const string &lb, int yy,const double * gr,int n);
virtual ~LabeledSalese(){}
const string & Label()const{return label;}
virtual double operator [](int i)const;
virtual double & operator [](int i);
private:
string label;
};
#endif // SALES_H
//下面的程序没有声明为内联的方法的实现,注意,对于被嵌套类的方法,需要使用多个作用域解析运算符。
//另外,如果数组索引超界,函数operator[]()将引发异常。
#include"sales.h"
#include<string>
using namespace std;
Sales::bad_index::bad_index(int ix, const string &s):logic_error(s),bi(ix)
{}
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=0;
for(i=0;i<lim;++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];
}
LabeledSalese::nbad_index::nbad_index(const string &lb, int ix, const string &s):Sales::bad_index(ix,s)
{
lbl=lb;
}
LabeledSalese::LabeledSalese(const string &lb, int yy):Sales(yy)
{
label=lb;
}
LabeledSalese::LabeledSalese(const string &lb, int yy, const double *gr, int n):Sales(yy,gr,n)
{
label=lb;
}
double LabeledSalese::operator [](int i)const
{
if(i<0||i>=MONTHS)
throw nbad_index(Label(),i);
return Sales::operator [](i);
}
double & LabeledSalese::operator [](int i)
{
if(i<0 || i>=MONTHS)
throw nbad_index(Label(),i);
return Sales::operator [](i);
}
//下面的程序中使用了这些类:首先试图超越LabeledSales对象sales2数组的末尾,然后试图超越Sales对象sales1中数组的末尾。
//这些尝试在两个try块中进行的,能够检查每中异常。
#include <iostream>
#include"sales.h"
using namespace std;
int main()
{
double vals1[12]=
{
1220,1100,1122,2212,1232,2334,
2884,2393,3302,2922,3002,3544
};
double vals2[12]=
{
12,11,22,21,32,34,
28,29,33,29,32,35
};
Sales sales1(2011,vals1,12);
LabeledSalese sales2("Blogstar",2012,vals2,12);
cout<<"First try block:\n";
try
{
int i;
cout<<"Year="<<sales1.Year()<<endl;
for(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(i=0;i<=12;++i)
{
cout<<sales2[i]<<' ';
if(i%6==5)
cout<<endl;
}
cout<<"End of try block 1.\n";
}
catch(LabeledSalese::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<<"\nNext try block:\n";
try
{
sales2[2]=37.5;
sales1[20]=23345;
cout<<"End of try block 2.\n";
}
catch(LabeledSalese::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<<"done\n";
return 0;
}
三、异常何时会迷失方向
异常被引发后,在两种情况下,会导致问题:
- 若在带异常规范的函数中引发的,必须与规范列表中的某种异常匹配(在继承乘次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常。在默认情况下,这将导致程序异常终止(输入c++11摒弃了异常规范,但任支持)。
- 若异常不是在函数中引发的(或者函数没有异常规范),必须捕获它。如果没有捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常异常。在默认情况下,这将导致程序异常终止。然而可以修改程序对意外异常和未捕获异常的反应。
- 未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数
terminate()
。 - 在默认情况下,terminate()调用abort()函数。可以指定terminate()应调用的函数(而不是abort())来修改terminate()的这种行为,为此,可调用
set_terminate()
函数。
set_terminate()和terminate()
都是在头文件exception
中声明的:
typedef void (*terminate_handler)();//指向没有参数和返回值的函数的指针
terminate_handler set_terminate(terminate_handler f) throw();//c++98
terminate_handler set_terminate(terminate_handler f) noexcept;//c++11
void terminate();//c++98
void terminate() noexcept;//c++11
//set_terminate()函数将不带任何参数且返回类型为void 的函数的名称(地址)作为参数,并返回该函数的地址。
//调用了set_terminate()函数多次,则terminate()将调用最后一次set_terminate()调用设置的函数。
例:假设希望未捕获的异常导致程序打印一条消息,然后调用exit()函数,将退出状态值设置为5,包含头文件:exception。可以使用using编译指令,适当的using声明或std::限定符,来使其声明可用:
#include<exception>
using namespace std;
函数如下:
void myQuit()
{
cout<<"Terminating due to uncaught exception\n";
exit(5);
}
最后,在程序的开头,将终止操作作指定为调用该函数。
set_terminate(myQuit);
现在,如果引发了一个异常且没有被捕获,将调用terminate(),而后者将调用MyQuit(),意外异常。通过给函数指定异常规范,可以让函数的用户知道要捕获那些异常。假设函数的原型如下:
double Argh(double double) throw(out_of_bounds);
则可以这样使用该函数:
try{
x=Argh(a,b);
}
catch(out_of_bounds & ex)
{
...
}
- 指定应捕获那些异常很有帮助,因为默认情况下,未捕获的异常将导致程序异常终止。
- 异常规范应包含函数调用的其他含引发的异常。例如:如果Argh()调用了Duh()函数,后者引发retort对象异常,则Argh()和Duh()的异常规范中都应包含retort。除非自己编写所有的函数,否则无法保证正确的完成。
若函数引发了其异常规范中没有的异常,结果如何?
- 这时,行为与未捕获的异常及其类似,如果发生意外异常,程序将调用
unexpected()
函数。 - 这个含调用
terminate()
,后者在默认情况下将调用abort()
。
正如有一个可用于修改terminate()
的行为的set_terminate()
函数一样,也有修改unexpected()
的行为的set_unexpected()
函数。
这些函数必须在头文件:exception
中:
typedef void (*unexpeced_handler)();
unexpected_handler set_unexpected(unexpected_handler f) throw();//c++98
unexpected_handler set_unexpected(unexpected_handler f) noexcept;//c++11
void unexpected();//c++98
void unexpected() noexcept;//c+0x
与提供给set_terminate()
的函数相比,提供给set_unexpected()
的函数的行为受更严格的限制,具体说,unexpected_handler
函数可以:
-
通过调用terminate()(默认行为),abort()或exit()来终止程序;
-
引发异常;引发异常(第二种选择)的结果取决于
unexpected_handler
函数所引发的异常以及引发意外异常的函数的异常规范。 -
如果新引发的异常与原来的异常规范匹配,则程序将从哪里开始进行正常处理,即寻找与新引发的异常匹配的catch块。基本上,这种方法将用预期的异常取代意外异常;
-
如果新引发的异常与原来的异常规范不匹配,且异常规范中没有包括
bad_exception
类型,则程序将调用terminate()
。bad_exception
是从exception派生而来的,其声明位于头文件exception
中。 -
如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了
bad_exception
类型,则不匹配的异常将被bad_exception
异常所取代。
总之,要捕获所有的异常(不管是预期的异常还是意外异常),则可以这样做:
#include<exception>
using namespace std;
//然后,设计一个替代函数,将意外异常转换为bad_exception异常,该函数的原型如下:
void myUnexpected()
{
throw bad_exception();
}
仅使用throw,而不是指定异常将重新引发原来的异常。然而,如果异常规范中包含了这种类型,则该异常将被bad_exception
对象取代。
接下来在程序的开始位置,将意外异常操作指定为调用该函数:
set_unexpected(myUnexpected);
最后,将bad_exception类型包括在异常规范中,并添加如下catch块序列:
double Argh(double double) throw(out_of_bounds,bad_exception);
...
try{
x=Argh(a,b);
}
catch(out_of_bounds & ex)
{
...
}
catch(bad_exception & ex)
{
...
}
四、有关异常的注意事项
讨论如何使用异常:应在设计程序时加入异常处理功能,而不是以后再添加。但这样有缺点。例如:使用异常会增加程序代码,降低程序的运行速度。异常规范不适合用于模板,因为模板函数引发的异常可能随特定的具体化而异。异常和动态内存分配并非总能协同工作。
例:
void test1(int n)
{
string mesg("I'm trapped in an endless loop");
...
if(oh_no)
throw exception();
...
return ;
}
string 类采用动态内存分配。通常,当函数结束时,将mesg调用string的析构函数。虽然throw语句过早地终止了函数,但它任然使得析构函数被调用,这归功与栈解退。
例:
void test2(int n)
{
double * ar =new double[n];
...
if(oh_no)
throw exception();
...
delete [] ar;
return;
}
这时有个问题,栈解退时,将删除栈中的变量ar,但函数过早的终止意味着函数末尾的delete[]语句被忽略。指针消失了,但它指向的内存块未被释放,并且不可访问,总之,这些内存被泄漏了。
但可以避免。例如:可以在引发异常的函数中捕获该异常,在catch块包含一些清理代码,然后从新引发异常:
void test3(int n)
{
double * ar=new double [n];
...
try{
if(oh_no)
throw exception();
}
catch(exception & ex)
{
delete [] ar;
throw;
}
...
delete [] ar;
return;
然而,这将增加疏忽和产生其他错误机会。解决方法:这将用到智能指针模板。