条款13:以对象管理资源
Use objects to manage resources.
本章分为两个部分。
资源
所谓资源(resources),指的就是:
- 如果使用了它,将来一定要还给系统。如果不还给系统,就会发送糟糕的事情。
C++程序内,最常用的资源就是动态分配内存(如果分配的内存不归还给系统,就会导致内存泄漏),但是,内存只是需要进行管理的众多资源之一。其他比较常见的资源还有:
- 文件描述器(file descriptors)
- 互斥锁(mutex locks)
- 图形界面中的字型和笔刷
- 数据库连接
- 网络sockets
举个例子:
假设我们使用一个用来模拟投资行为的程序库(例如模拟股票、债券等等),其中各式各样的投资类型则继承自一个root class Investment:
class Investment { ... } //“投资类型”继承体系中的 root class
对于这个程序,程序库通过一个工厂函数(factory function),条款7供应特定的Investment对象:
Factory函数会“返回一个base class指针, 指向新生成的derived class对象”。
Investment* createInvestment(); //返回指针,指向Investment继承体系内的动态分配对象。
//调用者有责任删除它。这里为了简化,刻意不写参数。
就像上面所说,createInvestment的调用端在使用了函数返回的对象后,有责任将其删除。定义一个实现这个责任的函数f:
void f()
{
Investment* createInvestment(); //调用factory函数
...
delete pInv; //释放pInv所指对象
}
上面的代码中,虽然开起来是可行的,但是存在着诸多情况下,f无法删除它从createInvestment的投资对象:
- 原因之一可能是在“ … ”内的代码可能存在过早的return语句。
如果这样的return执行起来,控制流就绝不会触及delete语句。
- 另一种的原因则是“ … ”内的语句抛出异常。
此时控制流也不会触及delete语句。
- 总之,无论delete语句因为什么原因被忽略了,所泄漏的不仅是内含投资对象的内存,还有投资对象所保存的所有资源。
因此,为了确保createInvestment返回的资源总是被释放,需要将资源放入到对象内。当控制流离开f,该对象的析构函数会自动释放这些资源。实际上,换句话说:
- 将资源放到对象内,便可以依赖C++的“析构函数自动调用机制”确保资源被顺利释放。
auto_ptr
许多资源被动态分配在heap中而后用于单一区块或函数内。对于这些资源:
- 它们应该在控制流离开这个区块或函数时完成释放。
对于这一点,标准程序库提供了auto_ptr来实现这个目标。
- auto_ptr是一个“类指针(pointer-like)对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
auto_ptr的使用:
void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); //调用factory函数,使用pInv
... //经由auto_ptr的析构函数会自动删除pInv
}
RAII
由上面的例子,关于“以对象管理对象”的:
1,获得资源后立即放入到管理对象(managing object)之中。在上面的代码中,createInvestment返回的资源被当做它的管理者auto_ptr的初值。实际上,“以对象管理资源”通常被称为:
- 资源获得时机,便是初始化时机。(Resource Acquisition Is Initialization, RAII)
因为我们几乎总是在获得一笔资源后于同一语句内,用它初始化某个管理对象。另外,有的时候获得的资源被用来赋值(而非初始化)某个管理对象。不管是初始化还是赋值,每一笔资源都是在获得的同时立刻被放进管理对象之中。
2,管理对象(managing object)运用析构函数确保资源被释放。不管控制流是如何离开区块或函数,一旦对象被销毁了(例如当对象离开作用域),其析构函数就自动会被调用,于是资源就被释放了。如果资源释放的动作可能会抛出异常,可以参考条款8中的介绍。
由于auto_ptr被销毁时会自动删除它所指的东西,因此一定要注意,不要让多个auto_ptr同时指向同一个对象。如果对象被删除了一次一上,就会出现“未定义行为”的问题。
针对这个问题,auto_ptr有这样的一个特性来预防这一问题:
- 若是想要通过copy构造函数或者copy assignment操作符去复制它们,它们就会变成null,而复制所得的指针将会获得资源的唯一拥有权。
std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment返回物
std::auto_ptr<Investment> pInv2(pInv1); //(copy构造)现在pInv2指向这个对象,pInv1被设为null
pInv1 = pInv2; //(copy assignment)现在有变成了pInv1指向这个对象,pInv2被设为null
对于上面这种奇怪的复制行为,其底层的逻辑为:
- 受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。
因为这一特性,auto_ptr并不是管理动态分配资源的最好的选择。例如,STL容器就要求其元素要有“正常的”复制行为,因此这些容器就无法使用auto_ptr。