More Effective C++ 阅读笔记(七)--防止在析构函数和构造函数中发生资源泄漏 黑月亮 发表于 2005-10-3 20:37:00 |
在析构函数和构造函数中防止资源泄露 1.在析构函数防止资源泄露
问题引入:你的程序的关键部分就是这个函数,如下所示: void processAdoptions(istream& dataSource) { while (dataSource) { // 还有数据时,继续循环 ALA *pa = readALA(dataSource); //得到下一个动物 pa->processAdoption(); //处理收容动物 delete pa; //删除readALA返回的对象 } } 这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除pa。
这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。任何时候pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏。 改进后的堵塞泄漏很容易 : void processAdoptions(istream& dataSource) { while (dataSource) { ALA *pa = readALA(dataSource); try { pa->processAdoption(); } catch (...) { // 捕获所有异常 delete pa; // 避免内存泄漏 // 当异常抛出时 throw; // 传送异常给调用者 } delete pa; // 避免资源泄漏 } // 当没有异常抛出时 } 但是你必须用try和catch对你的代码进行小改动。更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须写两个delete代码。象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。不论我们是让processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢? (WQ加注,VC++支持try…catch…final结构的SEH。) 防止内存泄漏的方法:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源.
标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的。每一个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是auto_ptr类的一些重要的部分: template<class T> class auto_ptr { public: auto_ptr(T *p = 0): ptr(p) {} // 保存ptr,指向对象 ~auto_ptr() { delete ptr; } // 删除ptr指向的对象 private: T *ptr; // raw ptr to object }; auto_ptr类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用。(我们至少必须加上拷贝构造函数,赋值operator和将在条款M28讲述的pointer-emulating函数),但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将不再为堆对象不能被删除而担心,即使在抛出异常时,对象也能被及时删除。(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对象数组的指针。如果想让auto_ptr类似于一个数组模板,你必须自己写一个。在这种情况下,用vector代替array可能更好。) 使用auto_ptr对象代替raw指针,processAdoptions如下所示: void processAdoptions(istream& dataSource) { while (dataSource) { auto_ptr<ALA> pa(readALA(dataSource)); pa->processAdoption(); } } 这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。第一,pa被声明为一个auto_ptr<ALA>对象,而不是一个raw ALA*指针。第二,在循环的结尾没有delete语句。其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是不是很容易。 隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息: // 这个函数会发生资源泄漏,如果一个异常抛出 void displayInfo(const Information& info) { WINDOW_HANDLE w(createWindow()); 在w对应的window中显式信息 destroyWindow(w); } 很多window系统有C-like接口,使用象like createWindow 和destroyWindow函数来获取和释放window资源。如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源一样。 解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源: //一个类,获取和释放一个window 句柄 class WindowHandle { public: WindowHandle(WINDOW_HANDLE handle): w(handle) {} ~WindowHandle() { destroyWindow(w); } operator WINDOW_HANDLE() { return w; } // see below private: WINDOW_HANDLE w; // 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝 //有关一个更灵活的方法的讨论请参见条款M28。 WindowHandle(const WindowHandle&); WindowHandle& operator=(const WindowHandle&); }; 通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示: // 如果一个异常被抛出,这个函数能避免资源泄漏 void displayInfo(const Information& info) { WindowHandle w(createWindow()); 在w对应的window中显式信息; } 即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。 资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。 2.在构造函数中防止资源泄露 问题引入: 为了实现某一个通信录,你可以这样设计: 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; // 他们的一段声音片段 }; 通讯录的每个条目都有姓名数据,所以你需要带有参数的构造函数,不过其它内容(地址、图像和声音的文件名)都是可选的。注意应该使用链表类(list)存储电话号码,这个类是标准C++类库(STL)中的一个容器类(container classes)。(参见Effective C++条款49 和More Effective C++条款M35) 编写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), 只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。 即当异常发生时调用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分配的内存仍旧被丢失了,这是因为如果new操作没有成功完成,程序不会对pb进行赋值操作。如果BookEntry的构造函数抛出一个异常,pb将是一个空值,所以在catch块中删除它除了让你自己感觉良好以外没有任何作用。用灵巧指针(smart pointer)类auto_ptr<BookEntry>(参见条款M9)代替raw BookEntry*也不会也什么作用,因为new操作成功完成前,也没有对pb进行赋值操作。 解决方案: 使用auto_ptr,把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的类)能化繁为简。它不仅把令人不好理解的代码隐藏起来,而且使得程序在面对异常的情况下也能保持正常运行。
|
|
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理