资源是这样一种东西:一旦借助它们所做的事情完成了,必须要将其返回给系统。如果没有这样做,那么不好的事情就会发生。在 C++ 程序中,最常用的资源是动态分配的内存(如果分配了内存但是却忘记了释放它们,程序就会遇到一次内存泄漏),但是内存只是所需要管理的众多资源中的一种。其它常见的资源包括文件主字码、互斥锁、以及图形用户界面( GUI )中的字体和画笔、数据库联接、网络套结字。无论是何种资源,在借助它所做的工作完成以后都要将其释放,这一点是很重要的。
试图手动将资源管理得井井有条,在任何情况下都是很困难的事情。但当问题转向异常处理、多路返回函数、以及当维护程序员在未对其所作的修改有充分理解之前就轻举妄动时,就会清楚地发现,专门用来解决资源管理问题的方法并不是很充足。
下面的示例中,我们的工作将围绕一个模拟投资(或者是股票、证券等等)的库展开,在这个库中,各种各样的投资类型都继承自同一个基类——Investment:
class Investment { ... }; // 投资类型层次结构的基类
供我们使用的库中,为我们提供具体Investment对象是通过工厂函数来实现的:
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 资源管理对象使用其析构函数来确保资源得到释放。由于析构函数是在对象销毁时自动调用的(比如,当对象将到达其作用域之外),所以不管程序是如何离开一个块的,资源都会被正确地释放。如果释放资源会带来异常,那么事情就会变得错综复杂。
由于当一个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在析构函数中都包含delete语句,而不是delete[]。这就意味着对于动态分配的数组使用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的相关内容。那儿会满足需求: Boost 提供了boost::scoped_array和boost::shared_array来处理相关问题。
本条目中建议我们始终使用对象来管理资源。如果我们手动释放资源(比如使用delete而不是使用资源管理类),我们就在做一些错事。诸如auto_ptr和TR1::shared_ptr等封装好的资源管理类通常可以让遵循本条目的建议变成一件很容易的事情,但是某些情况下,我们的问题无法使用这些预制的类来解决,此时便需要创建自己的资源管理类。但这并没有想象中那么难,但是确实需要考虑一些细节问题。
最后说一下,必须指出createInvestment的裸指针返回类型存在着潜在的内存泄露问题,因为调用者十分容易忘记在返回时调用delete。(甚至在它们使用auto_ptr或TR1::shared_ptr来运行delete时,他们仍然需要在一个智能指针对象中保存createInvestment的返回值。)解决这一问题需要改变createInvestment的对象。
牢记在心
l 为了避免资源泄露,可以使用 RAII 对象,使用构造函数获取资源,析构函数释放资源。
l auto_ptr或TR1::shared_ptr是两个常用并且实用的 RAII 类。通常情况下TR1::shared_ptr是更好的选择,因为它的复制行为更加直观。复制一个auto_ptr将会使其重设为null。
资源管理与RAII
本文介绍了资源管理的重要性,探讨了手动管理资源时可能出现的问题,并提出了使用资源获取即初始化(RAII)模式来解决这些问题的方法。文章还对比了auto_ptr与TR1::shared_ptr这两种资源管理工具的特点。

被折叠的 条评论
为什么被折叠?



