今天看了下Effective C++的条款13:以对象管理资源,感觉十分有理,特此做一下笔记。
假设我们使用一个用来描述投资行为的程序库,其中各式各样的投资类型都继承自一个根类 Investment:
//投资类型继承体系中的root class
class Investment{
...
};
这里呢,我们进一步假设这个程序库通过一个函数为我们提供Investment对象:
Investment* createInvestment();//指向动态分配对象,这里为了简化,我就不 //写参数了
如上所示,createInvestment函数的调用端使用了函数返回的对象后,就有责任删除这个对象。假设,有个函数f()执行这个删除操作:
void f()
{
IInvestment*pInv = createInvestment();
...//这里省去代码的细节
delete pInv;//释放所指对象
}
这个语句看起来感觉很合理,但是,假如我省略的代码细节里有个return语句,如此一来,我的f()函数在执行了return后就不会触及delete语句,也就不能释放那块内存。
所以,为了确保createInvestment()函数返回的资源总是能被释放,我们需要将资源放到对象内,当控制流离开函数f后,该对象的析构函数就会自动释放那些资源。这种做法就是本节内所讨论的问题:把资源放进对象内,依靠“C++析构函数自动调用机制”来确保资源被释放。
许多资源被动态分配于堆区(heap segment)内,而后就在某个函数或者区块内被使用。这些资源需要在函数调用完成或者离开区块的时候被释放。针对这个问题,C++标准程序库为我们提供了一些解决办法,比如auto_ptr(俗称智能指针)。auto_ptr是一个栈对象,他的析构函数自动对其对象调用delete函数,达到一个释放资源的作用。下面介绍一下auto_ptr避免f()函数内内存泄漏问题:
void f()
{
std::auto_ptr<Investment>pInv(createInvestment());
...//经由auto_ptr的析构函数自动释放pInv所指对象的内存(Effective //C++里说是删除pInv,可能是因为翻译的问题吧,感觉不对)
}
于是,这一个简单的小例子就阐明了“以对象管理资源”的两个关键的想法:
获得资源后立刻放进管理对象内。
上述代码中, createInvestment()函数的返回值直接作为了auto_ptr的对象的初值。实际上,这在C++里面有个形象的说法,就是RAII(Resource Acquisition Is Initialization,资源获取即初始化)。管理对象运用析构函数确保资源被释放。简而言之:只要对象离开作用域(比如离开函数的大括号)就自动调用析构函数,也就释放了资源(当然如果释放过程中出现了异常,就是比较麻烦的问题了,这一点放在后面去讨论,这里只关心资源释放的主要功能)。
特别注意的是,由于auto_ptr被销毁的时候回自动释放他所指向的那块内存,所以一定不能让多个auto_ptr同时指向同一个对象。否则,就会发生未定义的行为。为了预防这种问题,请大家像我一样遵循如下的规则:
如果通过拷贝构造函数或者析构函数复制auto_ptr,他们就会变成null,而复制所得的指针将得到资源的唯一所有权。
这一个规则我在这里用代码给大家进行一下简单的演示:
std::auto_ptr<Investment>pInv(createInvestment());
std::auto_ptr<Investment>pInv2(pInv);//情况1:调用拷贝构造函数
pInv1 = pInv2;//情况2:调用赋值函数
如上所示,情况1和2下都会产生同一个问题,pInv1和 pInv2都会变成null。
所以,为了解决这种诡异的现象,C++为我们提供了一个替代方案,也就是所谓的“引用计数型智慧指针”(RCSP,下面我都用这个词来简写)。TR1的shared_ptr就是常用的RCSP。
void f()
{
...
std::tr1::shared_ptr<Investment>pInv(createInvestment());
//当f执行完毕,离开大括号的时候,就会自动调用shared_ptr的析构函数,自动释放pInv所指的对象内存
}
这样的写法看似与auto_ptr类似,但是实际上,使用shared_ptr不会出现前面所说的那种置null现象,如下代码就可以看出差别:
void f()
{
...
std::tr1::shared_ptr<Investment>pInv(createInvestment());
std::tr1::shared_ptr<Investment>pInv2(pInv);
pInv = pInv2;//都没有置null
}
当然,除了以上的一些特点,还需要注意的是,auto_ptr与tr1::shared_ptr在析构函数内调用的都是delete,而不是delete[],所以,请注意:动态分配数组的时候,请勿使用auto_ptr与tr1::shared_ptr(尽管编译器不会报错,如果你非要用含有诸如auto_ptr与tr1::shared_ptr的类,那么请你去学一下Boost)。
小节:
- 为了防止内存泄漏,请务必使用RAII对象。他们会在构造函数中获取资源,在析构函数中delete资源。
- 常用的RAII对象是std::auto_ptr和std::tr1::shared_ptr,后面这个是较佳的选择。还有就是注意二者在调用拷贝构造函数和赋值函数时的区别。