前言
所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。C++程序中最常使用的资源就是动态分配内存(如果你分配内存却从来不曾归还它,会导致内存泄漏),但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁( mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。
尝试在任何运用情况下都确保以上所言,是件困难的事,但当你考虑到异常、函数内多重回传路径、程序维护员改动软件却没能充分理解随之而来的冲击,态势就很明显了:资源管理的特殊手段还不很充分够用。
本章一开始是一个直接而易懂且基于对象(object-based)的资源管理办法,建立在C++对构造函数、析构函数、copying函数的基础上。经验显示,经过训练后严守这些做法,可以几乎消除资源管理问题。然后本章的某些条款将专门用来对付内存管理。这些排列在后的专属条款弥补了先前一般化条款的不足,因为管理内存的那个对象必须知道如何适当而正确地工作。
正文
假设我们使用一个用来塑模投资行为(例如股票、债券等等)的程序库,其中各式各样的投资类型继承自一个root class Investment:
class Investment {}; // 投资类型继承体系中的root class
进一步假设,这个程序库系通过一个工厂函数(factory function,见条款7)供应我们某特定的Investment对象:
Investment* createInvestment(); // 返回指针,指向Investment继承体系内
// 的动态分配。调用者有责任删除它
// 这里为了简化,可以不写参数
一如以上注释所言,createInvestment的调用端使用了函数返回的对象后,有责任删除之。现在考虑有个f函数履行了这个责任:
void f() {
Investment* pInv = createInvestment(); // 调用factory函数
// ...
delete pInv; // 释放pInv所指对象
}
这看起来妥当,但若干情况下f可能无法删除它得自createInvestment的投资对象—或许因为"...”区域内的一个过早的return语句。如果这样一个return被执行起来,控制流就绝不会触及delete语句。类似情况发生在对createInvestment的使用及 delete 动作位于某循环内,而该循环由于某个continue 或goto语句过早退出。最后一种可能是..."区域内的语句抛出异常,果真如此控制流将再次不会幸临delete。无论delete如何被略过去,我们泄漏的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。
当然啦,谨慎地编写程序可以防止这一类错误,但你必须想想,代码可能会在时间渐渐过去后被修改。一旦软件开始接受维护,可能会有某些人添加return语句或continue语句而未能全然领悟它对函数的资源管理策略造成的后果。更糟的是f的"..."区域有可能调用一个“过去从未抛出异常,却在被‘改善’之后开始那么做”的函数。因此单纯倚赖“f总是会执行其delete语句”是行不通的。
为确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。实际上这正是隐身于本条款背后的半边想法:把资源放进对象内,我们便可倚赖CH+的“析构函数自动调用机制”确保资源被释放。(稍后讨论另半边想法。)
许多资源被动态分配于heap 内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr 正是针对这种形势而设计的特制产品。auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。下面示范如何使用auto_ptr以避免f函数潜在的资源泄漏可能性:
void f() {
std::auto_ptr pInv(createInvestment());
// 调用factory函数,一如既往地使用pInv,经由auto_ptr的析构函数自动删除pInv
// ...
}
这个简单的例子示范“以对象管理资源”的两个关键想法:
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。如果真是那样,对象会被删除一次以上,而那会使你的程序搭上驶向“未定义行为”的快速列车上。为了预防这个问题,auto_ptrs有个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!
std::auto_ptr
pInv1(createInvestment()); // pInv1指向createInvestment返回物
std::auto_ptr pInv2(pInv1); // 现在pInv2指向对象,pInv1被设为null
pInv1 = pInv2; // 现在pInv1指向对象,pInv2被设为null
这一诡异的复制行为,复加上其底层条件:“受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它”,意味 auto_ptrs 并非管理动态分配资源的神兵利器。举个例子,STL容器要求其元素发挥“正常的”复制行为,因此这些容器容不得auto_ptr.
auto_ptr的替代方案是“引用计数型智慧指针"reference-counting smart pointer;RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收( garbagecollection),不同的是RCSPs无法打破环状引用(cycles of references,例如两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”状态)。
TRl的tr1::shared _ptr(见条款54)就是个RCSP,所以你可以这么写f :
void f() {
// ...
std::tr1::shared_ptr pInv(createInvestment());
// 调用factory函数,使用pInv一如既往,经由shared_ptr析构函数自动删除pInv
// ...
}
这段代码看起来几乎和使用auto_ptr的那个版本相同,但shared ptrs的复制行为正常多了:
void f() {
std::tr1::shared_ptr
pInv1(createInvestment()); // pInv1指向createInvestment返回物
std::tr1::shared_ptr pInv2(pInv1); // 现在pInv2指向对象,pInv1被设为null
pInv1 = pInv2; // 现在pInv1指向对象,pInv2被设为null
}
// pInv1和pInv2被销毁,它们所指的对象也被自动销毁
由于tr1::shared_ptrs的复制行为“一如预期”,它们可被用于STL容器以及其他“auto_ptr之非正统复制行为并不适用”的语境上。
尽管如此,可别误会了,本条款并不专门针对auto_ptr, tr1 : :shared ptr或任何其他智能指针,而只是强调“以对象管理资源”的重要性,auto_ptr 和tr1 : :shared ptr只不过是实际例子。如果想知道tr1:shared_ptr的更多信息,请看条款14,18和54。
auto_ptr和 tr1::shared_ptr两者都在其析构函数内做 delete 而不是delete[]动作(条款16对两者的不同有些描述)。那意味在动态分配而得的array身上使用auto _ptr或tr1::shared ptr是个馊主意。尽管如此,可叹的是,那么做仍能通过编译:
std::auto_ptr aps(new std::string[10]);
// 馊主意,会用上错误的delete形式
std::tr1::shared_ptr spi(new int [1024]); // 相同问题
你或许会惊讶地发现,并没有特别针对“C++动态分配数组”而设计的类似auto_ ptr或tr1::shared_ptr那样的东西,甚至TR1中也没有。那是因为vector和string几乎总是可以取代动态分配而得的数组。如果你还是认为拥有针对数组而设计、类似auto_ptr和tr1 : :shared ptr那样的classes 较好,看看Boost吧(见条款55)。在那儿你会很高兴地发现boost::scoped_array 和boost::shared array classes,它们都提供你要的行为。
本条款也建议,如果你打算手工释放资源(例如使用delete而非使用一个资源管理类;resource-managing class),容易发生某些错误。罐装式的资源管理类如auto_ptr和 tr1 : :shared ptr往往比较能够轻松遵循本条款忠告,但有时候你所使用的资源是目前这些预制式classes无法妥善管理的。既然如此就需要精巧制作你自己的资源管理类。那并不是非常困难,但的确涉及若干你需要考虑的细节。那些考虑形成了条款14和条款15的标题。
作为最后批注,我必须指出,createInvestment返回的“未加工指针”(rawpointer)简直是对资源泄漏的一个死亡邀约,因为调用者极易在这个指针身上忘记调用delete。(即使他们使用auto_ptr或tr1::shared_ptr来执行delete,他们首先必须记得将createInvestment的返回值存储于智能指针对象内。)为与此问题搏斗,首先需要对createInvestment进行接口修改,那是条款18面对的事。
请记住