【more effective c++读书笔记】【第3章】异常(1)
条款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& address const 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; // 继续传递异常 } }但如果让theImage和theAudioClip都变成指针常量,即:class BookEntry{ public: .... //与前同 private: .... //与前同 const Image* theImage; const AudioClip* theAudioClip; };常量指针必须通过成员初始值列表初始化,如下:
BookEntry::BookEntry(const string& name, const string& address const 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& address const 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); else return 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& address const 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完成其应该完成的所有事情。
版权声明:本文为博主原创文章,未经博主允许不得转载。
posted on 2015-08-28 09:43 ruan875417 阅读(171) 评论(0) 编辑 收藏 举报