More effective c++ 条款10(下)

    技术2022-05-11  114

    条款10:在构造函数中防止资源泄漏(下) 

    你可能已经注意到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)。通常会这样初始化theImagetheAudioClip

    // 一个可能在异常抛出时导致资源泄漏的实现方法 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所指的对象不会被释放。而且我们不能通过在构造函数中增加trycatch 语句来解决问题,因为trycatch是语句,而成员初始化表仅允许有表达式(这就是为什么我们必须在 theImage theAudioClip的初始化中使用?:以代替if-then-else的原因)。

    无论如何,在异常传递之前完成清除工作的唯一的方法就是捕获这些异常,所以如果我们不能在成员初始化表中放入trycatch语句,我们把它们移到其它地方。一种可能是在私有成员函数中,用这些函数返回指针,指向初始化过的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;   } }

    上面的程序的确不错,也解决了令我们头疼不已的问题。不过也有缺点,在原则上应该属于构造函数的代码却分散在几个函数里,这令我们很难维护。

    更好的解决方法是采用条款9的建议,把theImage theAudioClip指向的对象做为一个资源,被一些局部对象管理。这个解决方法建立在这样一个事实基础上:theImage theAudioClip是两个指针,指向动态分配的对象,因此当指针消失的时候,这些对象应该被删除。auto_ptr类就是基于这个目的而设计的。(参见条款9)因此我们把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, theAddressthePhones一样。而且因为theImage theAudioClip现在是包含在BookEntry中的对象,当BookEntry被删除时它们能被自动地删除。因此不需要手工删除它们所指向的对象。可以这样简化BookEntry的析构函数:

    BookEntry::~BookEntry() {}                                      // nothing to do!

    这表示你能完全去掉BookEntry的析构函数。

    综上所述,如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造函数在存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值。

    在对象构造中,处理各种抛出异常的可能,是一个棘手的问题,但是auto_ptr(或者类似于auto_ptr的类)能化繁为简。它不仅把令人不好理解的代码隐藏起来,而且使得程序在面对异常的情况下也能保持正常运行。


    最新回复(0)