天天看点

More Effective C++之 Item M9:使用析构函数防止资源泄漏

对指针说再见。必须得承认:你永远都不会喜欢使用指针。

    Ok,你不用对所有的指针说再见,但是你需要对用来操纵局部资源(local resources)的指针说再见。假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的资料信息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate processing)。

    完成这个程序一个合理的方法是定义一个抽象类,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,所以为什么我们必须要在多个地方编写删除代码呢?

    (WQ加注,VC++支持try…catch…final结构的SEH。)

    我们可以把总被执行的清除代码放入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_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&);

};

    这看上去有些象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类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析构函数需要特殊的技术。你能在条款M10和条款M11中获取有关的知识。