第三章.
资源管理
资源是这样一种东西:一旦你借助它们所做的事情完成了,你必须要将其返回给系统。如果你没有这样做,那么不好的事情就会发生。在
C++
程序中,最常用的资源是动态分配的内存(如果你分配了内存但是却忘记了释放它们,你的程序就会遇到一次内存泄漏),但是内存只是你所需要管理的众多资源中的一种。其它常见的资源包括文件主字码、互斥锁、以及图形用户界面(
GUI
)中的字体和画笔、数据库联接、网络套结字。无论是何种资源,在你借助它所做的工作完成以后都要将其释放,这一点是很重要的。
试图手动将资源管理得井井有条,在任何情况下都是很困难的事情。但当问题转向异常处理、多路返回函数、以及当维护程序员在未对其所作的修改有充分理解之前就轻举妄动时,你就会清楚地发现,专门用来解决资源管理问题的方法并不是很充足。
本章以介绍一个基于对象的资源管理方法开始,该方法构建于
C++
所支持的构造函数、析构函数、拷贝操作符之上。实践显示,如果严格时刻坚持这一方法,便可以消灭资源管理中几乎全部的潜在问题。本章稍靠后一些的条目中将专门讲解内存管理的问题。这些条目是对前边较为一般化的条目的补充,因为管理内存的对象需要搞清楚如何正常工作。
第13条:
要使用对象来管理资源
在下面的示例中,我们的工作将围绕一个模拟投资(或者是股票、证券等等,均可)的库展开,在这个库中,各种各样的投资类型都继承自同一个基类——
Investment
:
class Investment { ... }; //
投资类型层次结构的基类
我们继续上面的示例,供我们使用的库中,为我们提供具体
Investment
对象是通过工厂函数(参见第
7
条)来实现的:
Investment* createInvestment(); //
返回一个指针,指向
Investment
//
层次结构中动态分配的对象,
//
调用者必须要将其删除
//
(为简化代码省略了参数表)
从上面代码中的注释中可以看出,当
createInvestment
的调用者完成对于
createInvestment
函数返回对象的操作后,这类调用者应负责删除这一对象。请看下边的代码,我们用
f
函数来承担这一责任:
void f()
{
Investment *pInv = createInvestment(); //
调用工厂函数
... //
使用
pInv
delete pInv; //
释放该对象
}
这看上去可以正常运行,但是
f
可能在一些情况下无法成功的删除来自
createInvestment
的对象。在上述代码的“
...
”部分可能存在不成熟的
return
语句。如果这样的
return
语句得到了执行,那么程序永远就不会转向
delete
语句执行。当在循环语句中使用
createInvestment
和
delete
时,会出现类似的情形,同时这样的循环也有可能因遇到
continue
或
goto
语句而提前退出。最后,“
...
”中的一些语句还有可能抛出异常。如果真的有异常抛出,程序同样也不会达到
delete
。无论
delete
是如何被跳过的,包含
Investment
对象的内存都有可能泄露,同时这类对象所控制的资源都有可能得不到释放。
当然,用心编程就有可能防止这类错误发生,但是请想象一下代码会多么的不固定——你需要不停地修改代码。在软件维护的过程中,为一个函数添加
return
或
continue
语句可能会对其资源管理策略造成怎样的影响呢,一些人可能由于不完全理解这一问题就这样做了。还有更严重的,就是
f
函数的“
...
”部分可能调用了一个这样的函数:它原先从不会抛出异常,但在其得到“改进”之后,它突然又开始能够抛出异常了。寄希望于
f
函数总能达到其中的
delete
语句并不可靠。
为了确保
createInvestment
所返回的资源总能得到释放,我们需要将这类资源放置在一个对象中,这一对象的析构函数应在程序离开
f
之后自动释放资源。实际上,这是本条目所蕴含的思想的一半:那就是将资源放置在对象中,我们可以寄希望于通过调用
C++
的默认析构函数,从而确保资及时源得到释放。
许多资源是在堆上动态分配的,并且仅仅在单一的程序块或函数中使用,同时这类资源应该在程序离开这一程序块或函数之前得到释放。标准库中的
auto_ptr
就是为这类情况量身定做的。
auto_ptr
是一个类似于指针的对象(一个智能指针),其析构函数可以自动地对用其所指的内容执行
delete
。以下的代码描述了如何使用
auto_ptr
来防止
f
潜在的资源泄露。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
//
调用工厂函数
... // pInv
的用法和原来相同
} //
通过
auto_ptr
的析构函数
//
自动删除
pInv
这一简单的示例向我们展示了使用对象管理资源的两大关键问题:
l
获取资源后,立即将资源转交给资源管理对象。
上边的示例中
,
createInvestment
返回的资源将初始化一个
auto_ptr
,从而实现对这类资源的管理。事实上,使用对象来管理资源的理念通常称为“资源获取即初始化”(
Resource Acquisition Is Initialization
,
简称
RAII
),这是因为在同一个语句中获取一个资源并且初始化一个资源管理对象是很平常的。某些时候获取资源就是为一个资源管理对象赋值,而不是初始化。但是无论是哪种途径,在获取资源时,每一个资源都都会立即转向一个资源管理对象。
l
资源管理对象使用其析构函数来确保资源得到释放。
由于析构函数是在对象销毁时自动调用的(比如,当对象将到达其作用域之外),所以不管程序是如何离开一个块的,资源都会被正确地释放。如果释放资源会带来异常,那么事情就会变得错综复杂。但是那第
8
条中介绍的内容,我们这里不关心这些。
由于当一个
auto_ptr
被销毁时,它
自动删除了其所指向的内容,所以永远不要让多个
auto_ptr
指向同一个对象,这一点很重要。如果你这样做了,这个对象就会被多次删除,这样你的程序就会陷入未知行为的陷阱。为了防止此类问题发生,
auto_ptr
有一个不同寻常的特性:如果你复制它们(通过拷贝构造函数或者拷贝赋值运算符),它们就会被重设为
null
,然后资源的所有权将由复制出的指针独占!
std::auto_ptr<Investment> // pInv1
指向
createInvestment
pInv1(createInvestment()); //
所返回的对象
std::auto_ptr<Investment> pInv2(pInv1);
//
现在
pInv2
指向这一对象,
// pInv1
被重设为
null
pInv1 = pInv2; //
现在
pInv1
指向这一对象
// pInv2
被重设为
null
在这一古怪的复制方法中,由于
auto_ptr
必须仅仅指向一个资源,因此增加了对于资源管理的潜在需求。这意味着
auto_ptr
并不适合于所有动态分配的资源。比如说,
STL
容器要求其内容的表现出“正常”的拷贝行为,所以
auto_ptr
的容器是不允许使用的。
引用计数智能指针(
reference-counting smart pointer
,
简称
RCSP
)是
auto_ptr
的一个替代品。一个
RCSP
是一个这样的智能指针:它可以跟踪有多少的对象指向了一个特定的
资源,同时当没有指针在指向这一资源时,智能指针会自动删除这一资源。可以看出,
RCSP
的行为与垃圾回收机很相似。然而,
RCSP
与垃圾回收机也不是完全一样的,它不能够打断循环引用(比如说,两个没有其它使用者的对象
互相指向对方)。
TR1
的
TR1::shared_ptr
就是一个
RCSP
,
于是你可以按下面的方式来编写
f
:
void f()
{
...
std::TR1::shared_ptr<Investment>
pInv(createInvestment()); //
调用工厂函数
... // pInv
的用法与前面相同
} //
通过
shared_ptr
的析构函数
//
自动删除
pInv
上面的代码与使用
auto_ptr
是几乎完全相同,但是复制
shared_ptr
的行为更加自然:
void f()
{
...
std::TR1::shared_ptr<Investment> pInv1(createInvestment());
// pInv1
指向
createInvestment
//
所返回的对象
std::TR1::shared_ptr<Investment> pInv2(pInv1);
//
现在
pInv1
与
pInv2
均指向
//
同一对象
pInv1 = pInv2; //
同上
—
因为什么都没有改变
...
} // pInv1
与
pInv2
被销毁,
//
它们所指向的对象也自动被删除了
由于复制
TR1::shared_ptr
的工作可以“如期进
行
”
,所以在
auto_ptr
会出现非正统的复制行为的地方,比如
STL
容
器以及其它一些上下文中,这类指针能够安全地应用。
但是,请不要迷失方向。本条目并不是专门讲解
auto_ptr
和
TR1::shared_ptr
的,也不是讲解智能指针的。本条目的核心内容是使用对象管理资源的重要性。
auto_ptr
和
TR1::shared_ptr
仅仅是这类对象的示例。(关于
TR1::shared_ptr
的更多信息,请参见第
14
、
18
和
54
条。)
auto_ptr
和
TR1::shared_ptr
在析构函数中都包含
delete
语句,而不是
delete[]
。(第
16
条中描述了二者的区别。)这就意味着对于动态分配的数组使用
auto_ptr
和
TR1::shared_ptr
不是一个好主意。但是遗憾的是,这样的代码会通过编译:
std::auto_ptr
<std::string> aps(new std::string[10]);
//
坏主意!
//
这里将使用错误的删除格式
std::TR1::shared_ptr
<int> spi(new int[1024]); //
同样的问题
你可能会很吃惊,因为在
C++
中没有类似于
auto_ptr
和
TR1::shared_ptr
的方案来解决动态分配数组的问题,甚至
TR1
中也没有。这是因为
vector
和
string
通常都可以代替动态分配的数组。如果你仍然希望存在类似于
auto_ptr
和
TR1::shared_ptr
的数组类,请参见
Boost
的相关内容(见
第
55
条
)。那儿会满足你需求:
Boost
提供了
boost::scoped_array
和
boost::shared_array
来处理相关问题。
本条目中建议你始终使用对象来管理资源。如果你手动释放资源(比如使用
delete
而不是使用资源管理类),你就在做一些错事。诸如
auto_ptr
和
TR1::shared_ptr
等封装好的资源管理类通常可以让遵循本条目的建议变成一件很容易的事情,但是某些情况下,你的问题无法使用这些预制的类来解决,此时你便需要创建自己的资源管理类。但这并没有想象中那么难,但是确实需要你考虑一些细节问题。这些细节问题就是第
14
和
15
条的主题。
最后说一下,我必须指出
createInvestment
的裸指针返回类型存在着潜在的内存泄露问题,因为调用者十分容易忘记在返回时调用
delete
。(甚至在它们使用
auto_ptr
或
TR1::shared_ptr
来运行
delete
时,他们仍然需要在一个智能指针对象中保存
createInvestment
的返回值。)解决这一问题需要改变
createInvestment
的对象,这是第
18
条的主题。
牢记在心
l
为了避免资源泄露,可以使用
RAII
对象,使用构造函数获取资源,析构函数释放资源。
l
auto_ptr
或
TR1::shared_ptr
是
两个常用并且实用的
RAII
类。通常情况
下
TR1::shared_ptr
是更好的选择,因为它的复制行为更加直观。复制一个
auto_ptr
将会使其重设为
null
。