所谓的“资源”,指的就是那些“一旦使用,将来必须要归还给操作系统”的组件。在C++程序当中,最常使用的资源就是堆内存;然而内存只是我们必须管理的众多资源之一,其它常用的资源还包括:文件描述符
、线程互斥量
、网络连接
等等。无论是哪一种资源,在使用完毕后都必须要归还给服务提供者。
尝试在任何情况下都手工维护资源,实际是一件很困难的事。因为一旦引入了多种执行流
、程序异常
等特性,那么手中的资源就很容易失去控制。
假使我们使用一个用来抽象投资行为的类库,其中各种各样的投资类型都继承自同一个基类Investment
:
class Investment { /* ... */ };
进一步,这个程序库通过一个工厂函数来提供某个特定的Investment
对象:
//此函数返回指针,
//指向一个Investment体系内的对象。调用者有责任手工删除它。
Investment *createInvestment();
如代码中的注释所言,工厂函数createInvestment
的调用者有责任手工删除掉获取到的对象。现在假设在业务逻辑当中,有一个函数function
调用了这个工厂函数,并进行了删除操作:
void function( /* ... */ )
{
Investment *invest = createInvestment();
//...
delete invest;
}
目前看起来程序工作良好,但是在很多情况下,函数function
无法删除这个对象:
- 可能在中间的某个逻辑中,函数认为功能已经完成,因此直接让函数返回,
invest
指向的对象就来不及释放; - 可能在中间的某个逻辑中,函数抛出了异常,则程序永远不会执行到
delete
语句。
无论delete
语句是如何被忽略的,我们浪费掉的不仅仅是单一的一个Investment
的子类对象,还包括这个对象内部包含的所有对象。
当然,精心地安排逻辑实现(例如在任何的一个代码分支中都提供一个相同的delete
语句),可以防止这一错误;但是其一,它造成了重复的代码;其二,在未来的更新迭代过程当中,可能会添加类似return
这样的语句,而全然未意识到它对资源管理造成的影响。因此,单纯地依赖开发者的资源管理意识和经验,这显然是不靠谱的。
为了确保createInvestment
工厂函数返回的对象资源总是可以被正确地回收,我们需要将这个资源放入一个对象当中:当控制流离开了函数后,这个对象的析构函数就自动地回收这个资源。也就是说:把资源放进对象内,就可以依赖C++的“析构函数离开作用域时将被自动调用”这一机制,确保资源被释放。
许多资源被动态分配在堆内存中,并仅在某一个作用域中使用;这些堆内存应该在离开这个作用域时立刻被回收。STL
提供了auto_ptr
来完成这项工作,它的析构函数自动对其所指的对象调用delete
:
void function()
{
std::auto_ptr<Investment> invest(createInvestment());
//...
}
这个例子实际上示范了“以对象管理资源”的两个关键思路:
- 获得资源后立刻放进管理对象。以上代码中
createInvestment
返回的资源被当作资源管理者auto_ptr
的初值;而实际上“以对象管理资源”的观念常被称为“资源获取即初始化”(RAII
)。因为我们几乎总是在获得某个资源后,在同一条语句内就创建出这个管理对象。 - 管理对象使用析构函数来确保资源被释放。无论控制流以怎样的方式结束当前作用域,一旦管理对象被销毁,那么其析构函数就自然会被自动调用,于是资源会被安全地回收。如果资源的回收过程中可能抛出异常,那么问题会变得有些棘手,但是条款8已经能够解决这个问题。
接下来原书中介绍了以上思想的两个实例:
shared_ptr
和unique_ptr
。
【注意】
为了放置资源泄露,请使用RAII
对象来管理资源的生命周期。