Bootstrap

C++使用析构函数防止资源泄漏

      完成这个程序一个合理的方法是定义一个抽象类,ALA("Adorable Little Animal"),然后为小狗和小猫建立派生类。一个虚拟函数processAdoption分别对各个种类的动物进行处理:

 class ALA {

public:

  virtual void processAdoption() = 0;

  ...

};

class Puppy: public ALA {

public:

  virtual void processAdoption();

  ...

};

class Kitten: public ALA {

public:

  virtual void processAdoption();

  ...

};

   你需要一个函数从文件中读去信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。这个工作非常适合于虚拟构造器(virtual constructor),在条款M25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:

// 从s中读去动物信息, 然后返回一个指针

// 指向新建立的某种类型对象

ALA * readALA(istream& s);

   你的程序的关键部分就是这个函数,如下所示:

void processAdoptions(istream& dataSource)

{

  while (dataSource) {                  // 还有数据时,继续循环

    ALA *pa = readALA(dataSource);      //得到下一个动物

    pa->processAdoption();             //处理收容动物

    delete pa;                         //删除readALA返回的对象

  }                                  

}

   这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除pa。这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。

   现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?processAdoptions没有捕获异常,所以异常将传递给processAdoptions的调用者。传递中,processAdoptions函数中的调用pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。结果,任何时候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,所以为什么我们必须要在多个地方编写删除代码呢?

      我们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。因为当函数返回时局部对象总是被释放,无论函数是如何退出的。(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)

   具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like对象(类指针对象)被释放时,我们能让它的析构函数调用delete。替代指针的对象被称为smart pointers(灵巧指针),参见条款M28的解释,你能使得pointer-like对象非常灵巧。在这里,我们用不着这么聪明的指针,我们只需要一个pointer-lik对象,当它离开生存空间时知道删除它指向的对象。

   写出这样一个类并不困难,但是我们不需要自己去写。标准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_Űㄡq<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&);

};

   这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见条款M27),有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。(参见条款M5 ,了解为什么你应该谨慎使用隐式类型转换操作)

  通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:

// 如果一个异常被抛出,这个函数能避免资源泄漏

void displayInfo(const Information& info)

{

  WindowHandle w(createWindow());

  在w对应的window中显式信息;

}

   即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。

   资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例如当你正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析构函数需要特殊的技术。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;