第13条:以对象来管理资源

来源:互联网 发布:网络被限制了怎么办 编辑:程序博客网 时间:2024/05/22 14:11
第三章 资源管理资源就是:一旦借助它所做的事情完成,必须要将其返回给系统。如果不这样,糟糕的事情就会发生。C++ 程序中最常使用的资源就是动态分配内存(如果分配了内存但是却忘记释放,就会导致内存泄漏),但是内存只是你所需要管理的众多资源这一。其它常见的资源包括文件描述器(file descriptors)、互斥锁、以及图形界面( GUI)中的字体和画笔、数据库联接、网络套结字。无论是何种资源,当不再使用以后都要将其释放。 尝试任何情况下都像上面那样是很困难的事情。但当问题转向异常处理、多路返回函数、以及程序维护员改动软件却没能充分理解随之而来的冲击,问题就很清楚了:资源管理的特殊手段还不是很够用。本章以介绍一个基于对象的资源管理方法为开始,建立在 C++的构造函数、析构函数、拷贝操作符基础上。实践显示,如果严格守这一方法,便可以消灭资源管理潜在的问题。本章稍靠后一些的条目中将专门讲解内存管理。是对前边条目的补充,因为管理内存的对象必须知道如何适当而正确地工作。
下面示例中,我们使用一个模拟投资(例如股票、证券等)的程序库,该库中各种投资类型都继承自同一个基类— Investment :class Investment { ... };          // 投资类型层次结构的基类
进一步,这个程序通过一个工厂函数(factory function 条款7),供应我们某特定的Investment对象:
Investment* createInvestment();// 返回一个指针,指向Investment 继承体系内动态分配的对象,// 调用者必须要将其删除 (为简化代码省略参数表)


从注释可以看出,当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


这个简单的例子示范“以对象管理资源”的两大关键问题:
1、获取资源后,立即将资源转交给资源管理对象。 上述代码中createInvestment返回的资源将初始化一个auto_ptr ,从而实现对这类资源的管理。事实上,使用对象来管理资源的理念通常称为“资源获取即初始化”( Resource Acquisition Is Initialization , 简称 RAII ),因为我们总是在获得一笔资源后于同一语句内以它初始化某个管理对象。某些时候获取资源被用于赋值某个管理对象(而不是初始化)。但无论何种做法,每笔资源都应在获得的同时立刻放进管理对象中。
2、资源管理对象运用析构函数确保资源得到释放。 由于析构函数在对象销毁时自动调用(例如对象敲开作用域),所以不管程序是如何离开一个块,资源都会被正确地释放。如果释放资源会带来异常,那么事情就会变得复杂。但是那第 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 的行为与垃圾回收很类似,但也并不完全一样,它不能够打断循环引用(例如两个没被使用的对象互指,因而好像处在“被使用”状态)
TR1 的TR1::shared_ptr (条款54)就是一个 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]);// 坏主意!// 这里将使用错误delete删除格式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 的raw指针返回类型有潜在的内存泄露问题,因为调用者十分容易忘记在返回时调用 delete 。(甚至在它们使用 auto_ptr 或 TR1::shared_ptr 来运行delete 时,他们仍然需要在一个智能指针对象中保存 createInvestment 的返回值。)解决这一问题需要改变 createInvestment 的对象,这是条款18的主题。 需要记住的1、为了避免资源泄露,可以使用RAII (Resource Acquisition Is Initialization)资源获取即初始化,使用构造函数获取资源,析构函数释放资源。2、auto_ptr 或TR1::shared_ptr 是 两个常用并且实用的RAII 类。  
0 0
原创粉丝点击