【more effective c++读书笔记】【第3章】异常(1)

来源:互联网 发布:惊天破仔仔换头像软件 编辑:程序博客网 时间:2024/06/14 06:40
条款9:利用Destructor避免泄漏资源
1、异常无法被忽略:如果一个函数利用“设定状态变量”的方式或是利用“返回错误码”的方式发出一个异常信号,无法保证此函数的调用者会检查那个变量或检验那个错误码。于是程序的执行可能会一直继续下去,远离错误发生地点。但是如果函数以抛出expcetions的方式发出异常信号,而该exception未被捕捉,程序的执行便会立刻中止。
2、例子:
class ALA{public:virtual void processAdoption() = 0;...};class Puppy : public ALA{public:virtual void processAdoption();...};class Kitten : public ALA{public:virtual void processAdoption()...};void processAdoptions(istream& dataSource){while (dataSource){ALA *pa = readALA(dataSource);pa->processAdoption();delete pa;}}
如果pa-> processAdoption ()函数抛出异常,processAdoptions无法捕捉它,这个异常会传播到processAdoptions的调用端。pa->process()之后的语句都会被跳过,不再执行,意味pa不会被删除,从而导致资源泄漏。
第一种解决方法:
void  processAdoptions(istream& dataSource){while (dataSource){ALA *pa = readALA(dataSource);try{pa->processAdoption();}catch (...){delete pa;throw;}delete pa;}}
上述函数被try语句和catch语句搞得乱七八糟,更重要的是,被迫重复撰写其实可被正常路线和异常路线共享的清理代码,本例指的是delete动作,这对程序的维护造成困扰。
第二种解决方法:以一个类似指针的对象取代指针pa。当类似指针的对象被销毁时,我们可以令它的析构函数调用delete。
void processAdoptions(istream& dataSource){while (dataSource){auto_ptr<ALA> pa(readALA(dataSource));pa->processAdoption();}}
隐藏在auto_ptr背后的观念——以一个对象存放必须自动释放的资源,并依赖该对象的destructor释放——亦可以以指针为本以外的资源施行。
3、坚持一个规则:把资源封装在对象内,通常便可以在exceptions出现时避免泄漏资源。
条款10:在constructors内阻止资源泄漏
例子:
class Image{  //给影像数据使用public:Image(const string& imageDataFileName);...};class AudioClip{ //给声音数据使用public:AudioClip(const string& audioDataFileName);...};class PhoneNumber{ ... }; // 用来放置电话号码class BookEntry{public:BookEntry(const string& name,const string& address = "",const string& imageFileName = "",const string& audioClipFileName = "");~BookEntry();....private:string theName;string theAddress;list<PhoneNumber> thePhones;Image* theImage;AudioClip* theAudioClip;};BookEntry::BookEntry(const string& name,const string& addressconst string& imageFileName,const string& audioClipFileName) :theName(name), theAddress(address), theImage(0), theAudioClip(0){if (imageFileName != "")theImage = new Image(imageFileName);if (audioClipFileName != "")theAudioClip = new AudioClip(audioClipFileName);}BookEntry::~BookEntry(){delete theImage;delete theAudioClip;}void testBookEntryClass(){BookEntry b("Addison-Wesley Publishing Company","One Jacob Way, Reading, MA 01867");...}

当构造函数中第二个new抛出异常时,第一个new的对象会导致内存泄漏。因为C++只会析构已构造完成的对象,而对象只有在其constructor执行完毕时才算完全构造妥当。如果exception在b的构造过程中被抛出,b的析构函数不会被调用。

如果将b分配与heap中,并在exception出现时调用delete:

void testBookEntryClass(){BookEntry *pb = 0;try {pb = new BookEntry("Addison-Wesley Publishing Company","One Jacob Way, Reading, MA 01867");...}catch (...) {                // 捕获所有异常delete pb;                // 删除pb,当抛出异常时throw;                    // 传递异常给调用者}delete pb;                   // 正常删除pb}

BookEntry构造函数里所分配的Image object还是泄漏了,因为除非new动作成功,否则上述那个赋值不会施加于pb身上。如果BookEntry构造函数抛出一个异常,pb将成为null指针,在catch块中删除它没有任何作用。

解决方法:

BookEntry::BookEntry(const string& name,const string& address,const string& imageFileName,const string& audioClipFileName): theName(name), theAddress(address),theImage(0), theAudioClip(0){try {   // 这try语句块是新加入的if(imageFileName != "") {theImage = new Image(imageFileName);}if (audioClipFileName != "") {theAudioClip = new AudioClip(audioClipFileName);}}catch (...) {                      // 捕获所有异常delete theImage;                 // 完成必要的清除代码delete theAudioClip;throw;                           // 继续传递异常}}
但如果让theImagetheAudioClip都变成指针常量,即:
class BookEntry{public:.... //与前同private:.... //与前同const Image* theImage;const AudioClip* theAudioClip;};

常量指针必须通过成员初始值列表初始化,如下:

BookEntry::BookEntry(const string& name,const string& addressconst string& imageFileName,const string& audioClipFileName) :theName(name), theAddress(address), theImage(imageFileName != ""?new Image(imageFileName):0),theAudioClip(audioClipFileName != "")? new AudioClip(audioClipFileName):0){}

导致上述问题仍然存在,解决方法:

class BookEntry{public:... //与前同private:... //与前同Image* initImage(constr string& imageFileName);AudioClip* initAudioClip(constr string& audioClipFileName);};BookEntry::BookEntry(const string& name,const string& addressconst string& imageFileName,const string& audioClipFileName) :theName(name), theAddress(address), theImage(0), theAudioClip(0), theImage(initImage(imageFileName)),theAudioClip(initAudioClip(audioClipFileName)){}//theImage首先被初始化,即使初始化失败也无须担心资源泄漏问题Image* BookEntry::initImage(const string& imageFileName){if (imageFileName != "") return new Image(imageFileName);else return 0;}//theAudioClip第二个被初始化,在它初始化期间有exception被抛出,必须将theImage//的资源释放掉,所以要用try...catch.AudioClip* BookEntry::initAudioClip(const string& audioClipFileName){try{if (audioClipFileName != "")return new AudioClip(audioClipFileName);elsereturn 0;}catch (...){delete Image;throw;}}

缺点:概念上应该由constructor完成的动作现在却散布于数个函数中,造成维护上的困扰。
更好的解决方法:

class BookEntry{public:.... //与前同private:constr auto_ptr<Image> theImage; //注意,改用auto_ptr对象cosntr auto_ptr<AudioClip> theAudioClip;//注意,改用auto_ptr对象};//在此设计中,如果theAudioClip初始化期间出现任何异常,theImage都是已构造好的对象,所以它会被自动销毁。BookEntry::BookEntry(const string& name,const string& addressconst string& imageFileName,const string& audioClipFileName) :theName(name), theAddress(address), theImage(0), theAudioClip(0), theImage(imageFileName != "" ? new Image(imageFileName) : 0),theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName){}//由于theImage和theAudioClip如今都是对象,当其“宿主”BookEntry被销毁时,它们亦将被自动销毁,无需手动方式删除它们BookEntry::~BookEntry() {}


条款11:禁止异常信息(exceptions)传递到析构函数外

1、两种情况下析构函数会被调用。第一种情况是当对象在正常状态下被销毁,也就是当它离开了它的生存空间或是被明确地删除。第二种情况是当对象被异常处理机制——也就是异常传播过程中的stack-unwinding(栈展开)——销毁。

2、如果控制权基于exception的因素离开destructor,而此时正有另一个exception处于作用状态,C++会自动调用terminate函数,将你的程序结束掉,甚至不等局部对象被销毁。

例子:

考虑一个用来监视在线计算的活动--也就是从你登录开始直到退出为止的所有行为的session class。每个session object都会记录其构造和析构的日期和时间。

class Session{public:Session();~Session();private:static void logCreation(Session* objAddr);static void logDestruction(Session* objAddr);};Session::~Session(){logDestruction(this);}

如果logDestruction抛出一个异常,这个异常并不会被Session Destructor捕捉,所以它会传播到destructor的调用端。但是万一这个destructor本身是因其他某个异常被调用的,terminate函数会被自动调用。

解决方法:

Session::~Session(){try{ logDestructor(this); }catch (...){}}

这个语句块阻止了logDestructor所抛出的exception传出Session Destructor之外。

3、如果exception从destructor内抛出,而没有在当地被捕获,那个destructor便执行不全(仅执行到抛出exception的哪一点为止)。如果destructor执行不全,就是没有完成它应该完成的每一件事情。

4、阻止异常传出destructor之外的两个好处:a、它可以避免terminate函数在异常传播过程的栈展开机制中被调用;b、它可以协助确保destructors完成其应该完成的所有事情。


0 0