0 前言
异常处理的本质是:当一个函数发现自己无法处理的错误时,抛出一个异常,希望它的(直接或间接)调用者能够处理这个问题。
为什么要处理异常?
如果我们写的代码永远不出现错误,内存永远够用,永远不存在无效指针。那么这个时候,我们不需要去考虑异常。然而上述前提条件是不存在的,异常考虑的是如果上述情况发生了,我们该怎么办!
面对异常,传统的处理技术有以下几个选项:
(1) 终止程序。
如异常未被捕获,次步骤默认发生。
(2)返回一个表示“错误”的值。
这种方式未必总是可行。例如:如果某个函数返回int,每个int 都是可能出现的结果。每次调用都需要做类型检查,很容易使程序规模加倍,显然这不是我们所愿看到的。
(3)返回一个合法值,让程序处于某种非法状态。
这种方法,也有问题。因为调用函数可能还没注意到程序已经处于一种非法状态。
(4)调用预先准备好,在出现“错误”情况下调用的函数。
异常处理并不意味着总是处理那些定义好的“错误”。
异常处理机制是在传统技术不充分,不优美和容易出错的时候提供的一种替代它们的技术。
1异常处理机制
C++语言结构:
如果想调用f()函数,并捕获它所throw的异常,我们可以写为:
try{
f();
}
catch(异常类){
//处理异常
}
检查一段错误代码,throw一个异常对象,catch这个异常并处理。
标准库中,包含很多个异常类,当然我们也可以为函数定义自己的异常类。
例如:
class Overflow{};
class Underflow{};
.........
void f(){
if(溢出)throw Overflow();
}
try{
f();
}
catch(Overflow){
if//能处理异常
else//处理不了的异常
throw;//往上一层继续抛出
}
然而往上一层抛出时,我们要留意一点,处理器网上抛出的只是原始异常的一个副本。
下面代码证明之:
省略号“...”表示“任何参数”,因此捕捉所有异常,我们可以写为:catch(...)。即为:
#include<iostream>
#include<string>
using std::cout;
using std::endl;
using std::exception;
using std::string;
class MyException:public exception{
string exc_info_;
public:
MyException(const char* exc_info):exc_info_(exc_info){
cout<<"the constructor called."<<endl;
}
void print(void){
cout<<exc_info_<<endl;
}
MyException(const MyException &tmp){
cout<<"the copy constructor called."<<endl;
exc_info_ = tmp.exc_info_;
}
~MyException(){
cout<<"the ~MyException called."<<endl;
}
};
void exceptionFun(){
try{
throw MyException("A Exception.");
}
catch(MyException &e){
e.print();
throw e;
}
}
int main(){
try{
exceptionFun();
}
catch(MyException &e){
//do nothing
}
}
省略号“...”表示“任何参数”,因此捕捉所有异常,我们可以写为:catch(...)。即为:
try{
//一些代码
}
catch(...){ //处理所有异常
//处理工作
}
派生类异常
对异常采用类层次结构,将会很自然地产生一些异常处理器。他们只对异常所携带的信息中的一个子集感兴趣。
例如:
class Matherr{
virtual void debug_print()const{
cout<<"Matherr."<<endl;
}
};
class IntOverflow:public Matherr{
int a1_;
int a2_;
public:
IntOverflow(int a,int b):a1_(a),a2_(b){}
void debug_print()const{
cout<<"IntOverflow."<<endl;
}
};
void f(){
try{
g();
}
catch(Matherr m){
//处理异常
}
}
即使g() throw一个IntOverflow的对象,进入Matherr处理器之后,IntOverflow对象所携带的附加信息是不可访问的。这里隐含着抛出异常因为捕捉而被“切割”。针对上述情况,我们可以利用指针或者引用来避免信息的永久性丢失。
try{
g();
}
catch(Mather &m){
//处理异常
}
异常处理器顺序
由于派生异常可能被多余一个异常类型的处理器捕捉,在写try语句的时候处理器的排列顺序就很重要了。
例如:
void g(){
try{
//.......
}
catch(...){
//处理所有异常
}
catch(std::exception &e){
//处理所有标准异常
}
catch(std::bad_cast){
//处理dynamic_cast失败
}
}
在这里bad_cast绝对不会被考虑,即使我们删掉catch(...),因为它是由exception派生而来的。正确的处理器顺序应该是反过来。
异常处理与检查
抛出或捕捉异常会对于一个函数和其他函数的关系产生影响。因此将可能抛出的异常作为函数声明的一部分就有价值了。
我们来看下面一个函数:
void f(int a)throw(X2,X3);
//如果一个函数的声明包含了异常的描述,这个函数的每个声明
//及实现都应该包含该异常描述集合。
这说明f()只能抛出两个异常:X2和X3,以及这些异常的派生类,不会抛出其他异常。
当某个函数描述了它可能抛出的异常,就为其调用者提供了一种保证。然而在函数的执行过程中做了某件事情,企图废除这种保证,这个企图就会转变为对std::unexpected()为预期异常的调用。而此函数将转而调用abort(),退出程序,因此上述例子可描述为:
void f()
try{
//......
}
catch(X2){
throw;
}
catch(X3){
throw;
}
catch(...){
std::unexpected();//unexpected()不会返回
}
这样做的优点在于函数的声明属于界面,函数的调用者可以清晰地看到函数可能抛出的异常。
如果函数的声明中不带任何的异常描述,那么就假定它可能抛出任何异常。
例如:int f(); //可能抛出任何异常
而不抛出异常的函数可以用空表声明:
而不抛出异常的函数可以用空表声明:
int f() throw(); // 不会抛出任何异常
要去覆盖一个虚函数,这个函数所带的异常描述必须至少是那个虚函数的异常描述一样受限(显示的或者隐式的)。
要去覆盖一个虚函数,这个函数所带的异常描述必须至少是那个虚函数的异常描述一样受限(显示的或者隐式的)。
例如:
class B{
public:
virtual void g() throw(X,Y);
virtual void h() throw();
};
class C:public B{
public:
void g() throw(X);//可以C::g()比B::g()受限
void h() throw(X,Y);//错误D::h()不如B::h()那么受限
};
与上述情况类似,你可以用一个指向具有更受限的异常描述符的指针给一个指向不那么受限的异常描述函数指针赋值,反过来则不可行。
例如:
void f() throw (X);
void (*pf1)() throw (X,Y) = &f; //OK
void (*pf2)() throw () = &f; //错误 f()不如pf2那么受限
特别地,你不能用一个指向没有异常描述符的函数的指针,给一个指向带有异常描述的函数的指针赋值。
特别地,你不能用一个指向没有异常描述符的函数的指针,给一个指向带有异常描述的函数的指针赋值。
例如:
void g(); // 可能抛出任何异常
void (*pf3)() throw (X) = &g; //错误 f()不如pf3那么受限
简单地说,函数指针赋值的时候左边的异常描述范围应该能够包含右边的异常描述范围。
还需要强调一点的就是异常描述并不是函数类型的一部分,typedef不能带有异常描述:
typedef void (*pf)() throw (X); //错误