简述
对于C程序猿来说,用来避免错误的技术已经很多了,为何还要引入C++的异常,并且异常还会带来一系列的问题。原因很简单:异常无法避免。
如果一个函数利用“设定状态变量”或者“返回错误码”的方式发出一个异常信号,无法保证此函数的调用者会检查那个变量或者错误码。于是程序执行会一直进行下去,远离错误发生点。但是如果函数以excption的形式抛出错误信号,而exception未被捕捉,则程序立即终止。
异常传递过程
class Exception{
public:
Exception(){}
};
void func(){
Exception e;
throw e;
}
int main(){
try{
func();
}catch(const Exception& e){
}
}
- 在
func
函数中throw e
,被抛出异常一定是e的一个拷贝,并不是e本身,不论catch端采用怎样的声明参数方式。抛出异常总是会发生复制。
class Base{...}
class Derive:public Base{...}
void(){
Derive local_derive;
Base& base = local_derive;
throw base;
}
- 当对象被复制为exception,其复制行为是由对象的copy constructor来实现的,而对象调用copy constructor是对应对象的静态类型而不是动态类型。这和其他的C++复制对象是一样的。
catch (Base& c){//方式1
throw;
}
catch (Base& c){//方式2
throw c;
}
重新抛出时应选择第一种方式。第一种方式只是“重新抛出”,不会改变对象的动态类型,而且不会重新创建异常对象。第二种方式会重新创建一个新的对象(效率低),而且创建对象是基于静态类型。
对于catch字句接受异常来说有三种方式,分为两类按值
catch (Base c)
,按引用catch (Base& c)
或catch (const Base& c)
。显然按照引用传递效率更高,免去了临时对象的产生和销毁。对于指针传递来说,要保证指针所指向的资源的有效性,局部变量会被销毁,导致指针无效;静态变量或者new出的对象虽然不会被销毁,但是应该由谁来销毁?,所以避免使用指针。
抛出异常的类型与catch中类型的匹配规则
在exception和catch语句类型匹配的过程中,仅存在两种转换
1. 继承架构中的转换。针对baseclass写的catch字句,可捕捉baseclass对象也可捕捉derivecalss对象。如标准库中的异常继承体系。
2. 从“有型指针”到“无型指针”的转换。如一个针对const void*的catch字句可以捕捉所有的指针类型的exception。
匹配顺序
异常的catch匹配采用的fisrt fit规则,第一个合适匹配就匹配。所以针对继承架构中的异常捕捉需要将子类放在前面,基类放到后面。
异常条件下避免资源泄漏
由于异常是随时可发生的,但是对于采用指针引用的资源来说,需要分配(new)和删除(delete)两个成对的操作来维护资源完整性。这就可能导致资源分配后,如果产生异常,资源将不会被delete,从而泄漏。
解决上述问题的一个技巧就是采用RAII,将获取的资源初始化一个对象,让这个对象的析构函数来负责释放资源。
对于在构造函数中将资源对象声明为采用智能指针的形式存储,可以保证在构造函数取得资源时产生异常的安全性。同时也可以简化析构函数的书写。
//不论在资源m_res1或者m_res2在获取时出现异常,资源都会被正常释放
class C{
public:
C():m_res1(new Resource),m_res2(new Resource){}
~C(){}
private:
const auto_ptr<Resource> m_res1;
const auto_ptr<Resource> m_res2;
};
总之资源采用智能指针的方式运用能够加强异常出现时的安全性。
禁止异常流出析构函数之外
两种情况下析构函数会被调用:
1. 对象在正常情况下销毁,离开作用域或者被明确的删除。
2. 对象被异常处理机制销毁。
但是在一个析构函数中无法判断是由于上述哪种原因引起的析构被调用。
所以编写析构函数必须考虑每种情况。
class C{
public:
C(){}
~C(){
DoWork();
}
};
上述代码在析构函数中调用了DoWork函数,但是如果此时DoWork产生了exception,这个exception并没有被~C()捕捉,所以exception会传播到析构函数的调用端,但万一这个析构是由于异常处理机制产生的调用,那么terminate函数会被自动调用,程序便终止。所以说,不要让异常流出析构函数。所以改为
class C{
public:
C(){}
~C(){
try{
DoWork();
} catch (...){}//保证不会从析构函数抛出任何异常
}
};
如果在析构中有多个函数执行
class C{
public:
C(){}
~C(){
DoWork1();
DoWork2();
DoWork3();
...
}
};
任意一个DoWord出现异常(不论是由情况1引起或者情况2引起),都有导致对象析构不完全,没有完成析构函数的完整工作。所以还要对每一个DoWork采用try/catch的方式包围起来。