条款13:以对象管理资源
所谓有资源,就是你一旦使用了它,就要记得归还系统。如果不这样做,就会发生资源的浪费。这里的资源不仅仅是指内存,也包括:文件描述符,互斥锁,数据库连接…
本条款是一个直接而易懂的基于对象的资源管理方法,建立在C++对构造函数,析构函数,copy函数的基础上。经验显示,经过训练后严守这些做法,可以几乎消除资源管理的问题。下面我们就开始了~~~~~
首先,我们以工厂模式来说起:
假如我们使用一个用来模拟投资行为的程序库,其中各式各样的投资类型继承自一个root class Investment;
classInvestment{};
进一步假设,这个程序库是通过一个工厂函数供应我们某特定的Investment对象:
Investment* createInvestment(){}//返回指针,指向Investment继承体系内的动态分配对象。调用者有责任删除它,这里为了简化,不写参数。
现在考虑有个f函数履行了这个责任:
voidf(){
Investment*pInv=createInvestment();
...
deletepInv;
}
这似乎看起来很美好,但是不幸的事情发生了,它发生在“…”代码中,可能是return了,也可能是出现了异常,总之,没有执行到delete上。我们泄露的不只是那些内存块,还有那些投资对象所保存的任何资源。
现在的问题是pInve和delete不在同一个对象,如果它们在同一个对象,我们完全可以依靠对象的析构函数来进行析构。因此……..
我们可以用标准库提供的auto_ptr。这是一个类指针对象。也就是所谓的智能指针。其析构函数自动对其所指对象调用delete。
voidf(){
std::auto_ptr<Investment>pInv(createInvestment());
...
}
这个简单的例子示范“以对象管理资源”的两个关键想法:
l 获得资源后,立刻放进管理对象内。
在以上代码中,createInvestment返回的资源被当作其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被称为“资源获取时机便是初始化时机”(Resource Acquisition Is Initializatioin, RAII).
l 管理对象运用析构函数确保资源被释放。
由于auto_ptr被销毁时,会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一个对象。如果真是那样,对象会被删除一次以上,会造成未定义行为。为了预防这个问题,auto_ptr有一个不寻常的性质:若通过copy函数复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。如下:
std::auto_ptr<Investment>pInv(createInvestment());//pInv point to a Investment obj
std::auto_ptr<Investment>pInv2(pInv);//pInv2 point to obj, pInv is null
pInv=pInv2();//pInv piont to obj, pInv2 is null
这一诡异的复制行为,复加上其底层乱条件:“受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它”。但是在STL中,容器要求其元素发挥“正常的”复制行为,因此这些容器不得auto_ptr。
Auto_ptr的一个替代方案就是“引用计数型智能指针(Reference-counting smart pointer, RCSP)”。所谓的RCSP也是一个智能指针。持续追踪共有多少个对象指向某笔资源,并在无人指向它时,自动删除该资源。
voidf()
{
std::tr1::shared_ptr<Investment>pInv1(createInvestment());//pInv1 point to a Investment obj
std::tr1::shared_ptr<Investment>pInv2(pInv1);//pInv1 and pInv2 point to the same obj;
pInv1=pInv2;//pInv1 and pInv2 point to the same obj;
}
Auto_ptr 和trl::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。那意味着在动态分配而得到的array身上使用Auto_ptr 和trl::shared_ptr是个坏主意。
请记住:
l 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
l 两个常使用的RAII classes 分别是Auto_ptr 和trl::shared_ptr。后者是一个较佳的选择。因为其copy行为比较直观。若选择前者,复制动作会使它指向null。