C++中利用析构避免资源泄露(RAII技术)

时间:2014.03.14

地点:基地

-------------------------------------------------------------------------

一、任务

  为收养中心写一个软件。收养中心每天都会产生一个文件,安排当天的收养个案,收养猫或者狗各有各的处理方式,我们写得程序需要读文件为每一个收养个案做处理。于是可以定义一个抽象基类。再从中派生出这对小猫小狗的具体类,重写虚函数因动物而异执行不同的动作。

class AnimalAdopt{
public:
  virtual void ProcessAdopt()=0;
  ...
};
class Pubby:public AnimalAdopt{
public:
  virtual void ProcessAdopt();
  ...
};
class Kitten:public AnimalAdopt{
  public:
  virtual void ProcessAdopt();
  ..
};

-------------------------------------------------------------------------

二、问题

  现在我们有一个函数,它能读取文件内容,并根据文件内容产生一个动物对象,以便针对该动物对象做应该有的处理,该函数声明如下:

//从s读取一条动物信息,返回一个指针,指向一个新分配的对象
AnimalAdopt* ReadAnimal(istream& s);
于是我们的核心人物任务实现如下:

void ProcessAoptions(istream& data_source){
  while(data_source){          //如果还有数据
    AnimalAdopt* pa=ReadAnimal)data_source);                           //取出下一个动物
  pa->ProcessAdoption();      //根据动物类型调用动物自己的成员函数处理事宜
  delete pa;                  //处理完毕,删除读出来得动物对象
  }
}
这个函数会遍历data_source,处理每一条动物信息。要记住的是在每次迭代后要删除ps,因为每次ReadAimal时,都会产生一个新的堆对象,要是处理完任务后不delete,很快就会发生资源泄露。

现在一切看似正常,但假如执行pa->ProcessAdoption时抛出了异常,这个异常就是被捕获,执行异常处理段,后面的代码会被跳过不再执行,即pa将不被理会,于是只有pa->ProcessAdoption没发生一次异常便发生一次资源泄露。

-------------------------------------------------------------------------

三、解决问题

如何避免,那就是在捕获异常,做异常处理的部分也delete资源,于是有:

void ProcessAdoptions(istream& data_source){
  while(data_source){
    AnimalAdopt *pa=ReadAnimal(data_source);
  try{
       pa->ProcessAdoption();
  }
  catch(...){  //捕获所有的异常
    delete pa;    //当异常抛出,避免资源泄露
    throw;  //将异常传播给调用端
   }
  delete pa;  //如果没有异常抛出也要避免资源泄露
  }
}
但这样做的后果是使得程序try语句块和catch语句块乱七八糟。更重要的是你被迫重复写正常路线和异常路线共享的清理代码。造成程序维护困难。

更好的办法:

  我们将一定得执行的清理代码移到函数的某个局部对象的析构函数内即可。因为局部对象总是会在函数结束是被析构。于是我们想以一个类似指针的对象取代指针pa。当这个类似指针的对象被自动销毁时,可以设置其析构函数调用delete。而该对象行为又类似指针,这就是我们常听闻的指针类似物——智能指针。我们的要求并不高端,只是要求在它被要求销毁之前删除他所指的对象。智能指针的设计正是为满足这种要求,每个auto_ptr的构造函数都要求获得一个指向堆对象的指针,析构函数则将堆对象删除。部分定义如下:

template
class auto_ptr{
public:
  auto_ptr(T* P=0):ptr(p){}  //存储对象
  ~auto_ptr(){delete ptr;}  //删除对象
private:
  T* ptr;  //原始指针
};  
以auto_ptr对象取代原始指针,就不必担心堆对象没被删除,即使在异常被抛出时。另外由于auto_ptr析构采用单一形式的delete。所以auto_ptr不适合包装数组对象的指针。于是上述问题的代码如下:

void ProcessAdoptions(istream& data_source){
while(data_source){
  auto_ptrpa(ReadAnimal(data_source);
  pa->ProcessAdoption();
  }
}
这样代码就显得简洁干净了。这就是以一个对象存放—必须自动释放的资源的观念。

-------------------------------------------------------------------------

四、应用

  考虑GUI应用中的函数需要产生一个窗口来显示信息。

void DisplayInfo(const information& info){
 WINDOW_HANDLE w(createWindown());
 //display info...
 destoryWindow(w);
}
现在来设计一个类,令其构造函数和析构函数分别取得资源和释放资源。
class WindowHandle{
public:
  WindowHandl(WINDOWNHADLE handle):w(handle){}
  ~WindowHandl(){destoryWindow(w);}
  operator WINDOW_HANDLE(){return w;}            //隐式
private:
  WINDOWN_HANDLE w;
  WindownHandle(const WindowHandle&);
  WindonHandle& operator=(const WindowHandle&);
};
这里我们使用的办法是将WINDOWNHANDLE对象封装到一个WindowHandle中,于是你现在就可以大胆去使用了,而不是出现在出现异常时资源未释放的问题,因为局部变量放在栈上,一旦调用结束会自动销毁。

总之,坚持这样一个原则:把资源封装在对象内,以免因为内存泄露的问题使你夜不能寐。



你可能感兴趣的:(C++编程,C++入门与实践)