More Effective C++----(10)在构造函数中防止资源泄漏
来源:互联网 发布:java中scanner用法案例 编辑:程序博客网 时间:2024/06/06 01:56
Item M10:在构造函数中防止资源泄漏
如果你正在开发一个具有多媒体功能的通讯录程序。这个通讯录除了能存储通常的文字信息如姓名、地址、电话号码外,还能存储照片和声音(可以给出他们名字的正确发音)。
为了实现这个通信录,你可以这样设计:
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(); // 通过这个函数加入电话号码 void addPhoneNumber(const PhoneNumber& number);... private: string theName; // 人的姓名 string theAddress; // 他们的地址 list<PhoneNumber> thePhones; // 他的电话号码 Image *theImage; // 他们的图像 AudioClip *theAudioClip; // 他们的一段声音片段};
编写BookEntry 构造函数和析构函数,有一个简单的方法是:
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;}
构造函数把指针theImage和theAudioClip初始化为空,然后如果其对应的构造函数参数不是空,就让这些指针指向真实的对象。析构函数负责删除这些指针,确保BookEntry对象不会发生资源泄漏。因为C++确保删除空指针是安全的,所以BookEntry的析构函数在删除指针前不需要检测这些指针是否指向了某些对象。
看上去好像一切良好,在正常情况下确实不错,但是在非正常情况下(例如在有异常发生的情况下)它们恐怕就不会良好了。
请想一下如果BookEntry的构造函数正在执行中,一个异常被抛出,会发生什么情况呢?:
if (audioClipFileName != "") { theAudioClip = new AudioClip(audioClipFileName);}
一个异常被抛出,可以是因为operator new(参见条款M8)不能给AudioClip分配足够的内存,也可以因为AudioClip的构造函数自己抛出一个异常。不论什么原因,如果在BookEntry构造函数内抛出异常,这个异常将传递到建立BookEntry对象的地方(在构造函数体的外面。 译者注)。
现在假设建立theAudioClip对象建立时,一个异常被抛出(而且传递程序控制权到BookEntry构造函数的外面),那么谁来负责删除theImage已经指向的对象呢?答案显然应该是由BookEntry来做,但是这个想当然的答案是错的。~BookEntry()根本不会被调用,永远不会。
C++仅仅能删除被完全构造的对象(fully contructed objects), 只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。所以如果一个BookEntry对象b做为局部对象建立,如下:
void testBookEntryClass(){ BookEntry b("Addison-Wesley Publishing Company", "One Jacob Way, Reading, MA 01867"); ... }
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}
C++拒绝为没有完成构造操作的对象调用析构函数是有一些原因的,而不是故意为你制造困难。原因是:在很多情况下这么做是没有意义的,甚至是有害的。如果为没有完成构造操作的对象调用析构函数,析构函数如何去做呢?仅有的办法是在每个对象里加入一些字节来指示构造函数执行了多少步?然后让析构函数检测这些字节并判断该执行哪些操作。这样的记录会减慢析构函数的运行速度,并使得对象的尺寸变大。C++避免了这种开销,但是代价是不能自动地删除被部分构造的对象。(类似这种在程序行为与效率这间进行折衷处理的例子还可以参见Effective C++条款13)
因为当对象在构造中抛出异常后C++不负责清除对象,所以你必须重新设计你的构造函数以让它们自己清除。经常用的方法是捕获所有的异常,然后执行一些清除代码,最后再重新抛出异常让它继续转递。如下所示,在BookEntry构造函数中使用这个方法:
BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName): theName(name), theAddress(address), theImage(0), theAudioClip(0){ try { // 这try block是新加入的 if (imageFileName != "") { theImage = new Image(imageFileName); } if (audioClipFileName != "") { theAudioClip = new AudioClip(audioClipFileName); } } catch (...) { // 捕获所有异常 delete theImage; // 完成必要的清除代码 delete theAudioClip; throw; // 继续传递异常 }}
不用为BookEntry中的非指针数据成员操心,在类的构造函数被调用之前数据成员就被自动地初始化。所以如果BookEntry构造函数体开始执行,对象的theName, theAddress 和 thePhones数据成员已经被完全构造好了。这些数据可以被看做是完全构造的对象,所以它们将被自动释放,不用你介入操作。当然如果这些对象的构造函数调用可能会抛出异常的函数,那么那些构造函数必须自己去考虑捕获异常并在继续传递这些异常之前完成必需的清除操作。
你可能已经注意到BookEntry构造函数的catch块中的语句与在BookEntry的析构函数的语句几乎一样。这里的代码重复是绝对不可容忍的,所以最好的方法是把通用代码移入一个私有helper function中,让构造函数与析构函数都调用它。
class BookEntry {public: ... // 同上 private: ... void cleanup(); // 通用清除代码}; void BookEntry::cleanup(){ delete theImage; delete theAudioClip;} BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName): theName(name), theAddress(address), theImage(0), theAudioClip(0){ try { ... // 同上 } catch (...) { cleanup(); // 释放资源 throw; // 传递异常 }} BookEntry::~BookEntry(){ cleanup();}
这似乎行了,但是它没有考虑到下面这种情况。假设我们略微改动一下设计,让theImage 和theAudioClip是常量(constant)指针类型:
class BookEntry {public: ... // 同上 private: ... Image * const theImage; // 指针现在是 AudioClip * const theAudioClip; // const类型};
必须通过BookEntry构造函数的成员初始化表来初始化这样的指针,因为再也没有其它地方可以给const指针赋值(参见Effective C++条款12)。通常会这样初始化theImage和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){}
这样做导致我们原先一直想避免的问题重新出现:如果theAudioClip初始化时一个异常被抛出,theImage所指的对象不会被释放。而且我们不能通过在构造函数中增加try和catch 语句来解决问题,因为try和catch是语句,而成员初始化表仅允许有表达式(这也是为什么我们必须在 theImage 和 theAudioClip的初始化中使用?:以代替if-then-else的原因)。
无论如何,在异常传递之前完成清除工作的唯一的方法就是捕获这些异常,所以如果我们不能在成员初始化表中放入try和catch语句,我们把它们移到其它地方。一种可能是在私有成员函数中,用这些函数返回指针指向初始化过的theImage 和 theAudioClip对象。
class BookEntry {public: ... // 同上 private: ... // 数据成员同上 Image * initImage(const string& imageFileName);AudioClip * initAudioClip(const string& audioClipFileName);}; BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName): theName(name), theAddress(address), theImage(initImage(imageFileName)), theAudioClip(initAudioClip(audioClipFileName)){} // theImage 被首先初始化,所以即使这个初始化失败也// 不用担心资源泄漏,这个函数不用进行异常处理。Image * BookEntry::initImage(const string& imageFileName){ if (imageFileName != "") return new Image(imageFileName); else return 0;} // theAudioClip被第二个初始化, 所以如果在theAudioClip// 初始化过程中抛出异常,它必须确保theImage的资源被释放。// 因此这个函数使用try...catch 。AudioClip * BookEntry::initAudioClip(const string& audioClipFileName){ try { if (audioClipFileName != "") { return new AudioClip(audioClipFileName); } else return 0; } catch (...) { delete theImage; throw; }}
更好的解决方法是采用条款M9的建议,把theImage 和 theAudioClip指向的对象做为一个资源,被一些局部对象管理。这个解决方法建立在这样一个事实基础上:theImage 和theAudioClip是两个指针,指向动态分配的对象,因此当指针消失的时候,这些对象应该被删除。auto_ptr类就是基于这个目的而设计的。(参见条款M9)因此我们把theImage 和 theAudioClip raw指针类型改成对应的auto_ptr类型。
class BookEntry {public: ... // 同上private: ... const auto_ptr<Image> theImage; // 它们现在是 const auto_ptr<AudioClip> theAudioClip; // auto_ptr对象};
这样做使得BookEntry的构造函数即使在存在异常的情况下也能做到不泄漏资源,而且让我们能够使用成员初始化表来初始化theImage 和 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){}
在这里,如果在初始化theAudioClip时抛出异常,theImage已经是一个被完全构造的对象,所以它能被自动删除掉,就象theName, theAddress和thePhones一样。而且因为theImage 和 theAudioClip现在是包含在BookEntry中的对象,当BookEntry被删除时它们能被自动地删除。因此不需要手工删除它们所指向的对象。可以这样简化BookEntry的析构函数:
BookEntry::~BookEntry(){} // nothing to do!
这表示你能完全去掉BookEntry的析构函数。
综上所述,如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造函数在存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值。
在对象构造中,处理各种抛出异常的可能,是一个棘手的问题,但是auto_ptr(或者类似于auto_ptr的类)能化繁为简。它不仅把令人不好理解的代码隐藏起来,而且使得程序在面对异常的情况下也能保持正常运行。
总而言之,就是用智能指针替换掉普通指针!!!
- More Effective C++----(10)在构造函数中防止资源泄漏
- More Effective C++之Item M10:在构造函数中防止资源泄漏
- C++在构造函数中防止资源泄露(9)---《More Effective C++》
- More Effective C++:防止资源泄漏
- 在构造函数中防止资源泄漏
- More Effective C++----异常 & (9)使用析构函数防止资源泄漏
- More Effective C++之Item M9:使用析构函数防止资源泄漏
- 在构造函数中避免资源泄漏
- More Effective C++:避免缺省构造函数
- more-effective-c++ 序列2 异常(第10节,在构造函数中抛出异常导致资源泄露)的测试示例
- C++:防止资源泄漏
- 《More Effective C++》学习心得(七) 构造函数私有化
- 《Effective C++》不要在构造函数和析构函数中调用虚函数
- More Effective C++----(4)避免无用的缺省构造函数 & (5)谨慎定义类型转换函数
- More Effective C++----技巧 & (25)将构造函数和非成员函数虚拟化
- 《More Effective C++》 4: 非必要不提供默认构造函数
- Effective C++:条款09:绝不在构造和析构过程中调用virtual函数
- 读书笔记《Effective C++》条款09:绝不在构造和析构过程中调用virtual函数
- 【自考总结】 信息资源管理
- linux下指令集
- 获取列表中多个相同元素的索引值
- Hibernate中Transaction事务的批量提交
- The type java.lang.Object cannot be resolved It is indirectly referenced ...
- More Effective C++----(10)在构造函数中防止资源泄漏
- OpenCV-Python教程(6)(7)(8): Sobel算子 Laplacian算子 Canny边缘检测
- Leetcode - Length of Last Word
- hdu2059
- 解决android中listview中嵌套checkbox滑动时checkbox状态改变问题
- Linux-27-linux基础重要命令04(L005-09)
- UVA 531Compromise
- vim 使用
- 查看文件夹