Item 13:使用对象(智能指针)来管理资源

使用对象管理资源

假设我们和一个投资(例如,股票,债券等)模型库一起工作,各种各样的投资形式从一个根类 Investment 派生出来:

class Investment { ... }; 

进一步假设这个库使用了通过一个 factory 函数为我们提供了特定的 Investment 对象的的方法:

//@ 返回一个 Investment 继承体系中动态分配的对象,调用者必须动态删除它
Investment* createInvestment();  

考虑一个 f 函数来履行以下职责:

void f()
{
  Investment *pInv = createInvestment();         //@ call factory function
  ...                                            //@ use pInv
  delete pInv;                                   //@ release object
}

有可能在这个函数的 "..." 部分的某处有一个提前出现的 return 语句。如果这样一个 return 执行了,控制流程就再也无法到达 delete 语句。还可能发生的一个类似情况是如果 createInvestment 的使用和删除在一个循环里,而这个循环以一个 continue 或 goto 语句提前退出。还有,"..." 中的一些语句可能抛出一个异常。如果这样,控制流程不会再到达那个 delete。无论那个 delete 被如何跳过,我们泄漏的不仅仅是容纳 investment 对象的内存,还包括那个对象持有的任何资源。

为了确保 createInvestment 返回的资源总能被释放,我们需要将那些资源放入一个类中,这个类的析构函数在控制流程离开 f 的时候会自动释放资源。

许多资源都是动态分配到堆上的,并在一个单独的块或函数内使用,而且应该在控制流程离开那个块或函数的时候释放。标准库的 auto_ptr 正是为这种情形量体裁衣的。auto_ptr 是一个类似指针的对象,它的析构函数自动在它指向的东西上调用 delete。下面就是如何使用 auto_ptr 来预防 f 的潜在的资源泄漏:

void f()
{
  std::auto_ptr<Investment> pInv(createInvestment());                                     
  ...                                             
}                 
  • 获得资源后应该立即移交给资源管理对象。如上,createInvestment 返回的资源被用来初始化即将用来管理它的 auto_ptr。实际上,因为获取一个资源并在同一个语句中初始化资源管理对象是如此常见,所以使用对象管理资源的观念也常常被称为 Resource Acquisition Is Initialization (RAII)。有时被获取的资源是被赋值给资源管理对象的,而不是初始化它们,但这两种方法都是在获取资源的同时就立即将它移交给资源管理对象。
  • 资源管理对象使用它们的析构函数确保资源被释放。因为当一个对象被销毁时(例如,当一个对象离开其活动范围)会自动调用析构函数,无论控制流程是怎样离开一个块的,资源都会被正确释放。

因为当一个 auto_ptr 被销毁的时候,会自动删除它所指向的东西,所以不要让超过一个的 auto_ptr 指向同一个对象非常重要。如果发生了这种事情,那个对象就会被删除超过一次,而且会让你的程序通过捷径进入未定义行为。为了防止这个问题,auto_ptr 具有不同寻常的特性:拷贝它们(通过拷贝构造函数或者拷贝赋值运算符)就是将它们置为空,拷贝的指针被设想为资源的唯一所有权。

std::auto_ptr<Investment>    pInv1(createInvestment());    
std::auto_ptr<Investment>    pInv2(pInv1);  
pInv1 = pInv2;                         

相对于 auto_ptr,另一个可选方案是一个引用计数智能指针(reference-counting smart pointer, RCSP)。一个 RCSP 是一个智能指针,它能持续跟踪有多少对象指向一个特定的资源,并能够在不再有任何东西指向那个资源的时候删除它。就这一点而论,RCSP 提供的行为类似于垃圾收集(garbage collection)。与垃圾收集不同的是,无论如何,RCSP 不能打破循环引用(例如,两个没有其它使用者的对象互相指向对方)。

shared_ptr 是一个 RCSP,所以你可以这样写 f:

void f()
{
  ...
  shared_ptr<Investment> pInv(createInvestment());       
  ...                                
}                                        

拷贝 shared_ptr 的行为却自然得多:

void f()
{
  ...
  std::tr1::shared_ptr<Investment>        pInv1(createInvestment());              
  std::tr1::shared_ptr<Investment>        pInv2(pInv1);                        
  pInv1 = pInv2;                           
  ...
}                                         

auto_ptr 和 shared_ptr 都在它们的析构函数中使用 delete,而不是 delete []。这就意味着将 auto_ptr 或 shared_ptr 用于动态分配的数组是个馊主意,可是,可悲的是,那居然可以编译:

auto_ptr<std::string>  aps(new std::string[10]);   
shared_ptr<int> spi(new int[1024]);   

总结

  • 为了防止资源泄漏,使用 RAII 对象,在 RAII 对象的构造函数中获得资源并在析构函数中释放它们。
  • 两个通用的 RAII 是 shared_ptr 和 auto_ptr。shared_ptr 通常是更好的选择,因为它的拷贝时的行为是符合直觉的。拷贝一个 auto_ptr 是将它置为空。
  • auto_ptr 和 shared_ptr 都在它们的析构函数中使用 delete,而不是 delete []。将 auto_ptr 或 shared_ptr 用于动态分配的数组是个馊主意。
posted @ 2020-01-12 19:04  刘-皇叔  阅读(167)  评论(0编辑  收藏  举报