Item13:以对象管理资源

0.概述

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII classes 分别是tr1 : :shared_ptr和 auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使被复制物指向null。

1.举例(依赖delete导致内存泄漏)

以一个模拟投资行为(股票、债券)的类为例,其中各种投资类型继承自一个root class Investment:

class Investment{...};

通过工厂函数供应特定的Investment对象:

Investment* createInvestment();  //返回指针,指向Investment继承体系中的动态分配对象

createInvestment的调用对象使用了其返回的对象后,有责任删除它。假设一个f函数履行了这个责任:

void f()
{
    Investment* pInv=createInvestment(); //调用工厂函数
    ...
    delete pInv; //释放对象
}

但在某些情况下,f可能无法删除其从createInvestment得来的对象:

  • 在释放对象之前有一个return语句被执行;
  • 对createInvestment的使用及delete对象位于某循环内,而该循环由于某个continue或goto语句过早退出;
  • 区域内的语句抛出异常。。。

后果:无论delete如何被略过去,我们泄漏的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。

从代码维护的角度,单纯依赖“f总会执行其delete语句”是行不通的。

为确保createInvestment返回的资源总是被释放,可倚赖C++的“析构函数自动调用机制”确保资源被释放。,将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。

2.智能指针auto_ptr

auto_ptr:类指针(智能指针),析构函数自动对其所指对象调用delete。下面示范使用auto_ptr以避免f函数潜在的资源泄露的可能性:

void f()
{
    std::auto_ptr pInv(createInvestment());
    ...//调用工厂函数,一如以往使用pInv对象,由auto_ptr的析构函数自动删除pInv
}

这个简单的例子示范了“以对象管理资源”的两个关键想法:

  • 获得资源后立刻放进管理对象( managing object)内。以上代码中createInvestment返回的资源被当做其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”( ResourceAcquisition Is Initialization; RAlI),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。
  • 管理对象(managing object)运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。 

由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。如果真是那样,对象会被删除一次以上。

为了预防这个问题,auto_ptrs有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!

std::auto_ptr pInv1(createInvestment());//pInv1指向createInvestment返回的对象
std::auto_ptr pInv2(pInv1); //现在pInv2指向对象,pInv1为空 
pInv1 = pInv2;//现在pInv1指向对象,pInv2为空

auto_ptr的这种神奇的复制行为,意味着它并非管理动态分配资源的好方法。举个例子,STL容器要求其元素发挥“正常的”复制行为,因此这些容器容不得auto_ptr。

3.引用计数型智慧指针(reference-counting smart pointer; RCSP)

是auto_ptr的替代方案。

所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。但RCSPs无法打破环状引用(cycles of references,例如两个其实已经没被使用的对象彼此互指,好像还处在“被使用”状态)。

TR1的tr1::shared ptr就是个RCSP,所以可以这么写f:

void f()
{
...
std::tr1::shared_ptr pInv(createInvestment()); 
... //调用工厂函数,一如以往使用pInv对象,由shared_ptr的析构函数自动删除pInv
}

与上面使用auto_ptr的代码非常相似,但shared ptr的复制行为正常多了。

void f()
{
...
std::tr1::shared_ptr pInv1(createInvestment());//pInv1指向createInvestment返回的对象
std::tr1::shared_ptr pInv2(pInv1);//pInv1和pInv2都指向对象 
pInv1 = pInv2; //同上
...
}//pInv1和pInv2被销毁,它们所指的对象也就被自动销毁.

由于tr1::shared ptrs的复制行为很正常,它们可被用于STL容器以及其他“auto_ptr的非正统复制行为并不适用”的语境上。

4.补充说明

auto_ptr和 tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。那意味在动态分配而得的array身上使用auto_ptr或tr1::shared ptr是个馊主意。但是这么干仍能通过编译!!(但是会用上错误的delete形式

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

:并没有特别针对“C++动态分配数组”而设计的类似auto_ptr或tr1: :shared_ptr那样的东西,甚至 TR1中也没有。那是因为vector和string几乎总是可以取代动态分配而得的数组。但 boost : : scoped_array和boost : :shared array classes可以提供这种功能。

如果你打算手工释放资源(例如使用celete而非使用一个资源管理类),容易发生某些错误。有时候你所使用的资源是auto_ ptr和 tr1 : :shared_ptr等预制式classes无法妥善管理的。既然如此就需要精巧制作你自己的资源管理类。这会涉及若干需要考虑的细节(见条款14和条款15)。

你可能感兴趣的:(Effective,C++,c++,开发语言)