exceptions:异常
9.利用destructors避免泄漏资源
使用智能指针可保证异常发生异常时不发送内存泄漏。
10.在constructor内阻止资源泄漏
若一个对象在constructor函数内发送了异常。那么这个对象是不完整的,new返回的是null指针,不能调用destructor ,会产生资源泄漏。
对此作出的策略是在constructor内捕捉可能发送的异常,进行清理工作。再继续传播这个异常。
classA::classA(const string& imageName):
image(0){
try{
if(imageName != ""){
image = new Image(imageName);
}
}
catch(...){
delete image;//清理
throw;//传播这个异常
}
}
更好的一种方式是把资源以智能指针方式存储在类内,发送异常可自动清理。
11.禁止异常流出destructors之外
阻止异常流出destructor之外的好处:1.避免terminate函数在异常传播过程的栈展开机制中被调用。2.可确保destructor完成应该完成的所有事情。
异常传播过程的栈展开:如果控制权基于异常的因素而离开destructor,而此时正有另一个异常处于作用状态,c++会调用terminate终结程序。
Session::~Session(){
logDestruction(this);
//若~Session由于异常被调用,而logDestruction又发生异常,将会调用terminate。
}
//应该改为:
Session::~Session(){
try{
logDestruction(this);
}
catch(...){}//不抛出异常
}
12.了解“抛出一个异常”与传递一个参数或”调用一个虚函数“之间的差异
把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛
出,这之间有三个主要区别。第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝。第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位 于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。
exception objects必定会造成复制行为:不论被捕捉的异常是以by value或by reference传递,交到catch子句手上的都是副本。因为传递过程中可能造成异常离开作用域被析构。
拷贝使用静态类型
当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数.
class Widget { ... };
class SpecialWidget: public Widget { ... };
void passAndThrowWidget()
{
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget; // rw 引用 SpecialWidget
throw rw; //它抛出一个类型为Widget的异常,因为rw的静态类型是Widget
}
重新抛出异常的类型
异常是其它对象的拷贝,这个事实影响到你如何在 catch 块中再抛出一个异常。比如下 面这两个 catch 块,乍一看好像一样:
catch (Widget& w)// 捕获 Widget 异常
{
...
throw; // 重新抛出异常,让它能继续传播
}
catch (Widget& w)
{
...
throw w; //传播被捕捉的异常的一个副本
}
这两个 catch 块的差别在于第一个 catch 块中重新抛出的是当前捕获的异常,而第二个 catch 块中重新抛出的是当前捕获异常的一个新的拷贝。第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。 特别是如果这个异常开始就是做为 SpecialWidget 类型抛出的,那么第一个块中传递出去的 还是 SpecialWidget 异常,即使 w 的静态类型(static type)是 Widget。这是因为重新抛 出异常时没有进行拷贝操作。第二个 catch 块重新抛出的是新异常,类型总是 Widget,因 为 w 的静态类型(static type)是 Widget。一般来说,你应该用 throw 来重新抛出当前的 异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。
异常类型的转换
在 catch 子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间 的转换。一个用来捕获基类的 catch 子句也可以处理派生类类型的异常。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有 const void* 指针的 catch 子句能捕获任何类型的指针类型异常:
void f(int value)
{
try {
if (someFunction()) {
throw value; //抛出一个整int
...
}
}
catch (double d) { // 只处理 double 类型的异常
...
}
...
}
在 try 块中抛出的 int 异常不会被处理 double 异常的 catch 子句捕获。该子句只能捕 获类型真真正正为 double 的异常,不进行类型转换。因此如果要想捕获 int 异常,必须使 用带有 int 或 int&参数的 catch 子句。
最先吻合
若catch一个基类的语句出现在其catch派生类语句前,那么catch派生类的语句永远不会执行,因为catch按照最先吻合策略,即在程序中找到可以用的便不再继续寻找。
13.以by reference方式捕获exceptions
使用catch by pointer的缺点
4个标准异常-bad_alloc(当 operator new不能分配足够的内存时,被抛出),bad_cast(当 dynamic_cast 针对一个引用操作失败时,被抛出),bad_typeid(当 dynamic_cast 对空指针进行操作时,被抛出)和 bad_exception(用于非预期的异常情况)都不是指向对象的指针,所以必须通过值或引用来捕获它们。
void someFunction()
{
static exception ex;
...
throw &ex;//抛出异常
...
}
void doSomething()
{
try {
someFunction();
}
catch (exception *ex) {//捕捉到异常
...
}
}
在捕捉到异常的时候,我们不能保证ex有没有因为离开其作用域而被销毁。如何使用分配于heap的异常对象,catch将不知道是否应该删除他。
使用catch by calue的缺点
它们被抛出时系统将对异常对象拷贝两次,并且会产生切割问题。即派生类的异常对象被做为基类异常对象捕获时, 那它的派生类行为就被切掉了。这样的 sliced 对象实际上是一个基类对象: 它们没有派生类的数据成员,而且当本准备调用它们的虚函数时,系统解析后调用的却是基类对象的函数。参考20
应该使用catch by reference
使用catch by reference,不会有对象删除的问题而且也能捕获标准异常类型,没有 slicing problem,而且异常对象只被拷贝一次。
14.明智运用exception specifications
exception specifications指出一个函数可以抛出什么样的异常,但是若抛出的异常并不在exception specifications内程序将会被终结。
有很多情况会导致exception specifications错误运用:
1.调用一个不知道会抛出什么样的异常的函数:
extern void f1();
void f2() throw(int){
f1();
}
我们不知道f1会不会抛出不属于f2的exception specifications内的异常。
如果A函数调用了B函数,而B函数无exception specifications,那么A函数也不用设定exception specifications
2.在template中
template<class T>
bool operator==(const T& lhs, const T& rhs) throw(){
return &lhs == &rhs;
}
若实例化的template重载了operator&,并且会抛出异常,那么将会血崩。
不应将Template与exception specifications混合使用
15.了解异常处理的成本
try和exception specifications都会增加成本,使代码体积膨胀,执行速度下降。抛出一个异常导致函数返回其速度对于正常的函数返回慢约3个数量级。