Effective C++条款13——以对象管理资源(资源管理)

前言

所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。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
    // ...
}

这个简单的例子示范“以对象管理资源”的两个关键想法:

  • 获得资源后立刻放进管理对象( managing object ) 内。以上代码中createInvestment返回的资源被当做其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”Resource Acquisition Is Initialization; RAIl),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值(而非初始化)某个管理对象,但不论哪一种做法,每一笔资源都在获得的同时立刻被放进管理对象中。
  • 管理对象((managing object)运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。如果资源释放动作可能导致抛出异常,事情变得有点棘手,但条款8已经能够解决这个问题,所以这里我们也就不多操心了。

由于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面对的事。

请记住

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII classes 分别是tr1::shared ptr和 auto ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

你可能感兴趣的:(Effective,C++,c++,开发语言,effective,c++,keep,studying,学习)