假设我们使用一个用来塑模投资行为(如股票、债券等等)的程序库,其中各式各样的投资类型继承自一个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,该对象的析构函数会自动释放那些资源。实际上这正是隐身于本条款内容里的部分想法:把资源放进对象内,我们便可倚赖C++的“析构函数自动调用机制”确保资源被释放(稍后讨论另一部分想法)。
许多资源被动态分配于heap内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形式而设计的特制产品。auto_ptr是个“类指针(pointer-like)对象”,即所谓的“智能指针”,其析构函数自动对其所指对象调用delete。下面示范如何使用auto_ptr以避免f函数潜在的资源泄露可能性:
void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); // 调用factory函数
// ... 一如既往地使用pInv
} // 经由auto_ptr的析构函数自动删除pInv
这个简单的例子示范“以对象管理资源”的两个关键想法:
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); // 现在pInv2指向对象,pInv1被设为null
pInv1 = pInv2; // 现在pInv1指向对象,pInv2被设为null
这一诡异的复制行为,加上其底层条件:“受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它”,意味着auto_ptr并非管理动态分配资源的神兵利器。举个例子,STL容器要求其元素发挥“正常的”复制行为,因此这些容器容不得auto_ptr。
auto_ptr的替代方案是“引用计数型智慧指针”(reference-counting smart pointer,RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSP提供的行为类似垃圾回收(garbage collection),不同的是RCSP无法打破环状引用(cycles of reference,例如两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”状态)。
TR1的tr1::shared_ptr(见条款54)就是个RCSP,所以你可以这么写f:
void f()
{
// ...
std::tr1::shared_ptr<Investment> pInv(createInvestment()); // 调用factory函数
// ... 使用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_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<std::string> aps(new std::string[10]); // 馊主意,会用上错误的delete形式
std::tr1::shared_ptr<int> spi(new int[1024]); // 相同问题
你或许会惊讶地发现,并没有特别针对“C++动态分配数组”而设计的类似auto_ptr或tr1::shared_ptr那样的东西,甚至TR1中也没有。那是因为vector和string几乎总是可以取代动态分配而得的数组。如果你还是认为拥有针对数组而设计、类似auto_ptr和tr1::shared_ptr那样的class较好,看看Boost吧(见条款55)。在那你会很高兴地发现boost::scoped_array和boost::shared_array class,它们都提供你要的行为。
本条款也建议,如果你打算手工释放资源(例如使用delete而非使用一个资源管理类(resource-managing class)),容易发生某些错误。罐装式的资源管理类如auto_ptr和tr1::shared_ptr往往比较能够轻松遵循本条款忠告,但有时你所使用的资源是目前这些预制式class无法妥善管理的。既然如此就需要精巧制作你自己的资源管理类。那并不是非常困难,但的确涉及若干你需要考虑的细节。那些考虑形成了条款14和15的标题。
作为最后批注,必须指出,createInvestment返回的“未加工指针”(raw pointer)简直是对资源泄露的一个死亡邀约,因为调用者极易在这个指针身上忘记调用delete(即使他们使用auto_ptr或tr1::shared_ptr来执行delete,他们首先必须记得将createInvestment的返回值存储于智能指针对象内)。为与此问题搏斗,首先需要对createInvestment进行接口修改,那是条款18面对的事。
请记住:
1.为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
2.两个常被使用的RAII class分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。