异常
条款9:使用析构函数防止资源泄漏
这一章我觉得可以算作是异常安全处理的一种情况,也就是说在异常发生的情况下,保证资源能被正确释放。
这里分为两种情况来讨论,
一是指针操作
举个例子:
void myFucntion()
{
Object* obj= getBack();
obj->doSomething();
delete obj;
}
如果在obj->doSomething()中发生异常,那么delete obj将不会执行,这是个很显然的泄露问题。想到的第一解决方法基本都是加上try...catch(),Ok那么让我们重新整理代码。
void myFucntion()
{
Object* obj= getBack();
try
{
obj->doSomething();
}
catch(...)
{
deleteobj;
}
delete obj;
}
好吧,我承认,程序很丑陋,代码重复,那么继续改进。
void myFucntion()
{
Object* obj= getBack();
try
{
obj->doSomething();
}
catch(...)
{
...
}
finally
{
deleteobj;
}
}
Ok,问题已经解决。但是还不要高兴,满篇的try catch,如果有很多地方都需要加try catch,那么工作量也是非常惊人的,当然你也可以这样写,但是我觉得作为程序员就是要有“懒惰”的思想,要简化,越简单越好,代码越少出错的地方也就越少。那么你一定想到了,利用智能指针auto_ptr或者share_ptr来解决问题。Ok,尽管不推荐auto_ptr,但是老的代码中还是会出现的。
[cpp] view plaincopyprint?
void myFucntion()
{
share_ptr<Object*> obj(getBack());
obj->doSomething();
}
Perfect!
二是对象操作
最常见的问题时windows中的各种handler的关闭泄露问题,我们可以针对handler进行一个封装,在析构函数中释放handler。
class WindowHandle
{
public:
WindowHandle(WINDOW_HANDLE handle): w(handle) {}
~WindowHandle() { destroyWindow(w); }
operatorWINDOW_HANDLE() { return w; } // see below
private:
WINDOW_HANDLE w;
WindowHandle(const WindowHandle&);
WindowHandle& operator=(const WindowHandle&);
};
条款10:在构造函数中防止资源泄漏
该条款讲述的内容跟上一条款很相似,应该都可以归属为编写异常安全代码范畴。在构造函数中尽量处理简单的代码,防止抛异常,因为C++并没有对构造函数异常做析构的动作,并不会在构造函数发生异常的情况下,调用析构函数去释放资源。这是考虑到效率问题。
我们还是举个例子更直观的描述如何在构造函数中编写异常安全的代码,要做个通讯录类,内容包括姓名,地址,图片和一段声音。
class Image
{
public:
Image(stringname);
private:
stringimageName;
};
class Audio
{
public:
Audio(stringname);
private:
stringaudioName;
};
class BookEntry
{
public:
BookEntry(string name, string adress, Image* image, Audio* audio);
~BookEntry();
private:
stringm_name;
stringm_adress;
Image*m_image;
Audio*m_audio;
};
BookEntry::BookEntry(string name, string adress,string imageName, string audioName)
: m_name(name),m_adress(adress),m_image(NULL),m_audio(NULL)
{
m_image = newImage(imageName);
m_audio = newAudio(audioName);
}
BookEntry::~BookEntry()
{
deletem_image;
deletem_audio;
}
因为C++ delete删除空null指针是不做任何操作的,是安全的,所以析构函数中并没有对指针进行判断。
我们考虑正常情况下构造函数全部指向完毕(完全构造),当销毁BookEntry的时候,会调用析构函数释放资源,没有任何问题。但是如果是构造函数中出现异常情况怎么办?
C++仅仅能删除被完全构造的对象(full constructedobjects),只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。C++拒绝为没有完成构造操作的对象调用析构函数是有一些原因的:如果为没有完成构造操作的对象调用析构函数,析构函数如何去做?仅有的办法是在每个对象里加入一些字节来指示构造函数执行了多少步?然后让析构函数检测这些字节并判断执行哪些操作。这样的记录会减慢析构函数的运行速度,并使得对象的尺寸变大。C++避免了这种开销,但是代价是不能自动删除被部分构造的对象.
最简单的办法就是万能的try catch。
[cpp] view plaincopyprint?
BookEntry::BookEntry(string name, string adress,string imageName, string audioName)
: m_name(name), m_adress(adress),m_image(NULL),m_audio(NULL)
{
try
{
m_image= new Image(imageName);
m_audio= new Audio(audioName);
}
catch(...)
{
deletem_image;
deletem_audio;
throw;
}
}
OK, 没有问题了,但是看起来catch中的代码跟析构函数的代码一样,OK,利用refactoring method我们可以合并代码,abstract a public method。
那么如果我们再修改下代码,将成员变量改为const常量
Image* const m_image;
Audio* const m_audio;
那么就只能在成员初始化列表中对指针进行初始化了。
BookEntry::BookEntry(string name, string adress,string imageName, string audioName)
: m_name(name),m_adress(adress),m_image((iamgeName.empty()?NULL:newImage(imageName))),m_audio((audioName.empty()?NULL:new Audio(audioName)))
{}
在成员初始化函数列表中,是不可以包含语句的,只能使用表达式,那么try catch就无法再使用了,那么该如何解决呢?估计看过前一章的一定会想到的,对就是使用智能指针。
const share_ptr<Image> m_image;
const share_ptr<Audio> m_audio;
BookEntry::BookEntry(string name, string adress,string imageName, string audioName)
: m_name(name),m_adress(adress),m_image((iamgeName.empty()?NULL:newImage(imageName))),m_audio((audioName.empty()?NULL:new Audio(audioName)))
{
}
BookEntry::~BookEntry()
{
}
连析构函数也省了。
如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造函数存在异常时发生资源泄露,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值
条款11:禁止异常信息传递到析构函数外
书中所言,会有两种情况调用析构函数。第一种是在正常情况下删除一个对象,例如对象超出作用域或者显示delete。第二种是异常传递的堆栈辗转开解(stack-unwinding)过程中,由异常处理系统删除一个对象。在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用 terminate 函数。
为什么要防止在析构函数中的异常传递到函数外,除了上面说的第二种情况外,还有就是将异常传递出去后,程序转到异常处理程序,析构函数中异常之后的程序将不会再被执行,这有可能会导致资源的泄露。那么析构函数就不会完全运行(它会停在抛出异常的那个地方上)
这里对stack unwinding说明下,堆栈展开是C++的一个概念,每个函数都回有它的堆栈,调用函数时会对函数参数和局部成员进行入栈,而函数结束时会按入栈相反的顺序进行出栈。举个例子说下的上面的第二种情况:
void func(string s)
{
object obj(s);
obj.dosomething();
}
如果在dosomething()中发生某些异常,那么异常将传递出去,直到遇到异常捕获函数为止。而随着异常的传递,函数func已经结束,其局部变量obj会被析构,如果在析构的过程中再次出现异常,那么C++将调用 terminate 函数,程序直接终止。
解决方法是在析构函数中使用try-catch块屏蔽所有异常
Session::~Session()
{
try{
logoDestruction(this);
}
Catch (…){ }
}
条款12:理解抛出一个异常与传递一个参数或调用一个虚函数间的差异
1.第一个差异:调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝。
2.C++规范要求被做为异常抛出的对象必须被复制(防止超出作用域被释放),即使被抛出的对象不会被释放(如static的),也会进行拷贝操作。这表示,即使通过引用来捕获异常,也不能在cacth块中修改原对象,仅仅修改的是原对象的拷贝
3.第二个差异:抛出异常运行速度比传递参数要慢。对象作为异常被抛出与作为参数传递给函数相比,前者类型转换比后者少(前者只有两种转换形式:继承类与基类的转换,类型化指针到无类型指针的转换);
4.在函数调用中不允许传递一个临时对象到一个非const引用类型的参数里,但是异常中却被允许
5.当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个
6.通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。但,你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免(注:也就是说,必须是全局的或堆中的,千万不要抛出一个指向局部对象的指针)
7.第三个差异:在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同
如 double sqrt( double ); //from <cmath> or <math.h>
我们能这样计算一个整数的平方根:
int i;
double sqrt0fi = sqrt( i );
毫无疑问,C++允许进行从int到double的隐式类型转换。一般来说,catch子句匹配异常类型时不会进行这样的转换,如:
void f( int value )
{
try{
if(someFunction() ) {
throw value;
...
}
}
catch( double d) {
...
}
}
在try块中抛出的int异常不会被处理double异常的catch子句捕获。该子句只能捕获类型真真正正为double的异常,不进行类型转换
8.catch子句中进行异常匹配时可以进行两种类型转换:第一种是继承类与基类间的转换;第二种允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常 catch (const void*)
9.最后一点差异:catch子句匹配顺序总是取决于它们在程序中出现的顺序。与这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的动态类型最相近的类里。你可以这样说虚拟函数采用最优适合法,而异常处理采用最先适合法。如果一个处理派生类异常的catch子句位于处理基类异常的catch子句后面,编译器会发出警告(这样的代码在C++里通常是不合法的)。不过你最好做好预先防范:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面,因为基类会处理派生类的异常
10 示例代码:
#include <iostream>
using namespace std;
class Base
{
public:
Base() {cout << "constructor" << endl; }
~Base() { cout << "destructor"<< endl; }
Base(constBase& m) { cout << "copy constructor" << endl; pInt =m.pInt; }
voidMessage() {cout << "Base::Message" <<endl;}
private:
int pInt;
};
class SubClass: public Base
{
public:
SubClass() {cout << "constructor_sub" << endl; }
~SubClass(){cout << "destructor_sub" << endl; }
SubClass(const SubClass& ) { cout << "copyconstructor_sub" << endl; }
voidMessage() {cout << "SubClass::Message" <<endl;}
private:
};
int main() {
int iTemp =0;
try
{
throwiTemp;
}
catch(double d)
{
cout<< "double" << endl;
}
catch (inti)
{
cout<< "int" << endl;
}
int* piTemp= NULL;
try
{
throwpiTemp;
}
catch (void*e)
{
cout<< "void*" << endl;
}
catch (int*i)
{
cout<< "int* " << endl;
}
SubClass n;
try
{
throw n;
}
catch(Base& e)
{
e.Message();
//throw;
//throwe;
}
catch (SubClass&ex)
{
e.Message();
}
Base m;
//Base* m =new Base();
try
{
throw m;
}
catch (Basee)
{
cout<< "Base" << endl;
}
// catch(Base& e)
// {
// cout<< "Base&" << endl;
// }
// catch(const Base& e)
// {
// cout<< "const Base&" << endl;
// }
// catch(Base* e)
// {
// e->Message();
// deletee;
// }
}
第一部分throw iTemp;输出:cout <<"int" << endl; 对应上面的第二种情况,异常传递的类型如果是基础类型的话不能进行隐式转换。
第二部分throw piTemp;输出:cout <<"void*" << endl;对应上面的第二种情况,说明如果是异常抛出指针的话,catch参数可以被隐式转换为void*指针。
第三部分throw n;输出:
constructor
constructor_sub
constructor
copy constructor_sub
Base::Message
destructor_sub
destructor
对应上面的第二种情况,说明在异常捕获中,子类异常可以通过基类参数捕获到。
对应上面的第三种情况,说明捕获的顺序是代码中出现的顺序。
把注释掉的//throw; //throw e;两句分别打开,运行结果确是一样的,跟书中描述不尽相同。理论上讲throw应该抛出的时subclass类型的异常。
最后一部分主要是验证异常参数传值,传引用和传指针的区别。通过程序结果可以看出,不论是传值还是传引用,都会进行拷贝构造,不同之处为传值会进行两次拷贝构造。不过在传指针时,确没有进行拷贝构造函数的调用。和函数参数穿指针一样。但是需要注意的是,异常处理有可能会跟异常抛出位置不是同一个作用域,那样抛指针异常的时候,有可能会造成异常泄露,所以我在最后显示调用delete e;释放资源。
条款13:通过引用&捕获异常
1.通过指针方式捕获异常是最高效的(能够不拷贝对象)。但如果catch子句接收到的指针,不是全局或静态的或堆的,那简直非常糟糕。就算是建立的一个堆对象,但catch子句无法判断是否应该删除该指针?如果是在堆中建立的,那必须删除,否则会资源泄漏。如果不是在堆中建立的异常对象,他们绝对不能删除它,否则程序的行为将不可预测。这是不可能知道的。而且通过指针捕获异常也不符合C++语言本身的规范,所以最好避开它
2.通过值捕获异常时系统将对异常对象拷贝两次(一次建立临时对象,一次拷贝--条款12),而且它会产生slicing problem,即派生类的异常对象被做为基类异常对象捕获时,那它的派生类的行为就被切掉了(sliced off),这样的sliced对象实际上是一个基类对象(当一个对象通过传值方式传递给函数,也会发生一样的情况)
3.通过引用捕获异常,就不会为删除异常对象而烦恼;能够避开slicing异常对象;能够捕获标准异常类型;减少对象需要被拷贝的数目 catch (exception & ex)
条款14:审慎使用异常规格
何为异常规格,通俗的理解就是对异常的规范的说明。它明确地描述了一个函数可以抛出什么样的异常。但是它不只是一个有趣的注释。编译器在编译时有时能够检测到异常规格的不一致。而且如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。异常规格既可以做为一个指导性文档同时也是异常使用的强制约束机制。
不过在通常情况下,美貌只是一层皮,外表的美丽并不代表其内在的素质。函数unexpected缺省的行为是调用函数terminate,而terminate缺省的行为是调用函数abort,所以一个违反异常规格的程序其缺省的行为就是halt(停止运行)。在激活的栈中的局部变量没有被释放,因为abort在关闭程序时不进行这样的清除操作。对异常规格的触犯变成了一场并不应该发生的灾难。
一个函数调用了另一个函数,并且后者可能抛出一个违反前者异常规格的异常,(A函数调用B函数,但因为B函数可能抛出一个不在A函数异常规格之内的异常,所以这个函数调用就违反了A函数的异常规格 译者注)编译器不对此种情况进行检测,并且语言标准也禁止编译器拒绝这种调用方式(尽管可以显示警告信息)。
1.避免调用unexcepted的第一个方法:避免在带有类型参数的模板内使用异常规格。因为我们没有办法知道某种模板类型参数抛出什么样的异常,所以不可能为一个模板提供一个有意义的 异常规格;
2.避免调用unexcepted的第二个方法:如果在一个函数内调用其他没有异常规格的函数时应该去除这个函数的异常规格
3.避免调用unexcepted的第三个方法:处理系统本身抛出的异常可以将所有的 unexpected异常都被替换为自定义的异常对象,或者替换unexpected函数,使其重新抛出当前异常,这样异常将被替换为 bad_exception,从而代替原来的异常继续传递
4.虽然防止抛出unexpected异常是不现实的,但是C++允许你用其它不同的异常类型替换unexpected异常,你能够利用这个特性。例如你希望所有的unexpected异常都被替换为UnexpectedException对象。你能这样编写代码:
class UnexpectedException {}; // 所有的unexpected异常对象被
//替换为这种类型对象
void convertUnexpected() // 如果一个unexpected异常被
{ // 抛出,这个函数被调用
throwUnexpectedException();
}
set_unexpected(convertUnexpected);
条款15:了解异常处理的系统开销
在理论上,异常是C++的一部分,C++编译器必须支持异常。三个方面:
第一需要空间建立数据结构来跟踪对象是否被完全构造,还需要系统时间保持这些数据结构不断更新;
第二try块无论何时使用它,都得为此付出代价,编译器为异常规格生成的代码与它们为try块生成的代码一样多,所以一个异常规格一般花掉与try块一样多的系统开销。
第三抛出异常的开销因为异常很少见,所以这样的事件不会对整个程序的性能造成太大的影响
为了使你的异常开销最小化,只要可能就尽量采用不支持异常的方法编译程序,把使用try块和异常规格限制在你确实需要它们的地方,并且只有在确为异常的情况下才抛出异常。如果你还是有性能上的问题,请利用分析工具(profiler)分析你的程序(item16)。