读书笔记 effective c++ Item 13 用对象来管理资源

1.不要手动释放从函数返回的堆资源

假设你正在处理一个模拟Investment的程序库,不同的Investmetn类型从Investment基类继承而来,

1 class Investment { ... }; // root class of hierarchy of
2
3 // investment types

进一步假设这个程序库通过一个工厂函数(Item 7)来给我们提供特定Investment对象:

1 Investment* createInvestment(); // return ptr to dynamically allocated
2
3 // object in the Investment hierarchy;
4
5 // the caller must delete it
6
7 // (parameters omitted for simplicity)

正如注释所表述的,当createInvesment返回的对象不再被使用时,调用者有责任将此对象释放掉。我们用函数f来履行这个职责:

 1 void f()
 2
 3 {
 4
 5 Investment *pInv = createInvestment(); // call factory function
 6
 7 ... // use pInv
 8
 9 delete pInv; // release object
10
11 }

这个方法看上去挺好,但是在一些情况下释放从createInvestment得来的对象有可能会失败。在函数的”…”部分中有可能会出现过早的reture语句,如果这个return被执行了,那么最后的delete语句永远不会被执行到;如果createInvesment和delete在一个循环中,break和goto语句会使循环过早退出,delete也不会被执行到;最后在…中的一些语句有可能会抛出异常,如果这样的话,控制流程会再次不能执行到delete。不管delete是怎么被跳过去的,不仅会泄露Invesment对象所使用的内存,也会泄露Investment对象所拥有的任何资源。

当然,小心的编程可以防止这类错误的发生,但是你应该想到随着时间的推移代码有可能发生变化。在软件的维护过程中,一些人可能在没有完全领会这个函数的资源管理策略的情况下为其添加一个return或者continue语句。更糟糕的是,f函数的”…”部分有可能调用一个从来没有抛出异常的函数,但这个函数被“改善”后,它抛出异常了。所以依赖f来到达delete语句通常是不可行的。

2.通过对象来管理需要手动释放的资源

为了确保从createInvestment返回的资源总是被释放,我们需要将资源放到一个对象中,当离开函数f的时候,对象的析构函数会自动释放对象拥有的资源。事实上,我们已经说出了这个条款一半的内容:通过将资源放入对象中,我们可以依赖c++的析构函数自动调用机制来确保资源被释放。(另一半一会就会讲到)

2.1 使用auto_ptr来管理资源

许多资源是被动态的分配在堆上的,它们被用在一个单独的块或者函数中,当控制流离开块或者函数时,这些资源应该被释放。标准库中的auto_ptr正是为这种情况量身定做的。Auto_ptr是一个指针(智能指针)一样的对象,它的析构函数会自动为其指向的对象调用delete函数。下面演示如何使用auto_ptr来防止可能出现的资源泄露:

 1 void f()
 2
 3 {
 4
 5 std::auto_ptr<Investment> pInv(createInvestment()); // call factory
 6
 7 // function
 8
 9 ... // use pInv as
10
11 // before
12
13 } // automatically
14
15 // delete pInv via
16
17 // auto_ptr’s dtor

2.2 用对象管理资源的两个关键点

这个简单的例子指出了使用对象管理资源的两个关键点:

  • 获取资源后应该立即将其转交给资源管理对象。从上面的例子看出,使用createInvestment返回的资源来初始化对其进行管理的auto_ptr指针。事实上,用对象来管理资源的想法通常被叫做”资源获取的时候就是初始化的时候”(Resource Acquisition Is Initialization RAII),因为将资源获取和资源管理对象的初始化放在同一个语句中是非常常见的。有时用获取的资源给资源管理对象赋值而不是初始化,但是不管哪种方法,都是在资源获取到之后马上将控制权转交给资源管理对象。
  • 资源管理对象使用它们的析构函数来确保资源被释放。因为不管控制流是怎么离开块或函数的,对象销毁的时候析构函数会被自动调用(例如当一个对象超出了作用域),资源因此能够被正确释放。释放资源时抛出异常会使问题变的棘手,这个问题在Item8中讨论了,我们不再担心这种问题。

因为 当auto_ptr被销毁时会自动delete它所指向的资源,所以有没有多个auto_ptr指向通一个对象是很重要的。如果有多个,对象会被多次delete,这就会导致出现未定义行为。为了防止这样的问题出现,auto_ptrs有一个与众不同的性质:被拷贝的指针(通过拷贝构造函数或者拷贝赋值运算符)会被置为null,进行拷贝的指针将拥有资源的所有权

 1 std::auto_ptr<Investment> // pInv1 points to the
 2
 3 pInv1(createInvestment()); // object returned from
 4
 5 // createInvestment
 6
 7 std::auto_ptr<Investment> pInv2(pInv1); // pInv2 now points to the
 8
 9 // object; pInv1 is now null
10
11 pInv1 = pInv2; // now pInv1 points to the
12
13 // object, and pInv2 is null

2.3 用shared_ptr来管理资源

奇特的拷贝行为,加上“不能有超过一个的auto_ptr指向被auto_ptr管理的资源”,这两种特性使得auto_ptrs不是管理所有动态分配资源的最好方法。举个例子,STL容器需要”正常的”拷贝行为,因此就不能将容器放入auto_ptr中。

Auto_ptr的一种替代方法是使用“引用计数的智能指针”(reference-counting smart pointer RCSP).RCSP是一种能够跟踪有多少对象指向同个一特定资源的指针,资源只有在没有指针指向的情况下才能被释放。因此,RCSP提供的行为同垃圾回收机制类似。和垃圾回收机制不同的是,RCSP不会制止循环引用(例如,两个都不被使用的对象却指向彼此,看上去在被使用一样。)

TR1的tr1::shared_ptr(看Item54)是是一个RCSP,所以你可以这么实现f:

 1 void f()
 2
 3 {
 4
 5 ...
 6
 7 std::tr1::shared_ptr<Investment>
 8
 9 pInv(createInvestment()); // call factory function
10
11 ... // use pInv as before
12
13 } // automatically delete
14
15 // pInv via shared_ptr’s dtor

这段代码看上去同使用auto_ptr大致相同,但是拷贝shared_ptrs的行为更加自然:

 1 void f()
 2
 3 {
 4
 5 ...
 6
 7 std::tr1::shared_ptr<Investment> // pInv1 points to the
 8
 9 pInv1(createInvestment()); // object returned from
10
11 // createInvestment
12
13 std::tr1::shared_ptr<Investment> // both pInv1 and pInv2 now
14 pInv2(pInv1); // point to the object
15 pInv1 = pInv2; // ditto — nothing has
16 // changed
17 ...
18 } // pInv1 and pInv2 are
19 // destroyed, and the
20 // object they point to is
21 // automatically deleted

因为拷贝tr1::shared_ptrs的工作方式是你所想要的,它们可以被用在像STL容器和其他上下文中,在这里auto_ptr的古怪的拷贝方式不再合适。

2.4 不要将auto_ptr和shared_ptr用于动态分配数组

不要被误导。这个条款不是用来介绍关于auto_ptr,tr1::shared_ptr或者其它类型的智能指针。这个条款讲述的是用对象管理资源的重要性。使用Auto_ptr和tr1::shared_ptr只是举个例子。(关于tr1::shared_ptr的更多内容,查看Item14 18和54)

Auto_ptr和tr1::shared_ptr的析构函数中使用的是delete而不是delete[]。(Item16 描述了区别)这意味着在auto_ptr或者tr1::shared_ptr中存放动态分配的数组不是一个好方法,令人遗憾的是,这种用法可以通过编译:

1 std::auto_ptr<std::string> // bad idea! the wrong
2
3 aps(new std::string[10]); // delete form will be used
4
5 std::tr1::shared_ptr<int> spi(new int[1024]); // same problem

你会惊奇的发现c++中没有用于动态分配数组的类似auto_ptr或者tr1::shared_ptr的东西,TR1中也没有。因为vector和string基本可以替代动态分配数组了。如果你仍然认为存在用于动态分配数组的类似于auto_ptr和tr1::shared_ptr的类是好的,可以看一下Boost(Item 55).你会非常高兴的发现boost::scoped_array和boost::shared_array类提供了你正在寻找的。

3.其他问题

这个条款中,使用对象管理资源的指导方针意味着如果你自己手动释放资源(例如使用delete而不是一个资源管理类),你的做法就是错误的。 预装的资源管理类,像auto_ptr和tr1::shared_ptr使遵守这个条款变的更加容易,但有时候当你使用一个资源的时候你会发现这些预制的类没有做到你想要的。这种情况下,你就需要编写你自己的资源管理类了。这也不是非常难的,但确实有一些微妙的地方需要你考虑。这些注意点将要在Item14和Item15种进行讨论。

最后,我必须指出createInvestment的原生指针返回类型是资源泄露的导火索,因为调用者很容易就会忘记调用delete(即使使用auto_ptr和tr1::shared_ptr来执行delete,它们仍然需要记得将createInvestment的返回值放入智能指针对象中)。对付这个问题需要调用createInvestment的修订版本,这个问题会在Item18中进行讨论。

时间: 2024-08-05 11:29:24

读书笔记 effective c++ Item 13 用对象来管理资源的相关文章

读书笔记 effective c++ Item 49 理解new-handler的行为

1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨论推迟到本条款结束的时候. 1.1 调用set_new_handler来指定全局new-handler 在operator new由于不能满足内存分配要求而抛出异常之前,它会调用一个客户指定的叫做new-handler的错误处理函数.(这也不是完全正确的.Operator new的真正行为更加复杂.

读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C++标准预计在2009年被发布(虽然所有的工作很有可能在2007年底被完成).直到现在,发布下一版C++的预计年份还没有被确定,这就解释了为什么人们把下一版C++叫做“C++0x”——C++的200x年版本. C++0x可能会包含一些有趣的新的语言特性,但是大多数新C++功能将会以标准库附加物的形式被

读书笔记 effective c++ Item 11 在operator=中处理自我赋值

1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: 1 class Widget { ... }; 2 3 Widget w; 4 5 ... 6 7 w = w; // assignment to self. 这看上去是愚蠢的,但这是合法的,所以请放心,客户端是可以这么做的.此外,自身赋值也并不总是很容易的能够被辨别出来.举个例子: 1 a[i] = a[j]; // potential assignment to self 上面的代码在i和j相等的情况下就是自我赋值,同

读书笔记 effective c++ Item 45 使用成员函数模板来接受“所有兼容类型”

智能指针的行为像是指针,但是没有提供加的功能.例如,Item 13中解释了如何使用标准auto_ptr和tr1::shared_ptr指针在正确的时间自动删除堆上的资源.STL容器中的迭代器基本上都是智能指针:当然,你不能通过使用“++”来将链表中的指向一个节点的内建指针移到下一个节点上去,但是list::iterator可以这么做. 1. 问题分析——如何实现智能指针的隐式转换 真正的指针能够做好的一件事情是支持隐式转换.派生类指针可以隐式转换为基类指针,指向非const的指针可以隐式转换成为

读书笔记 effective c++ Item 38 通过组合(composition)为 “has-a”或者“is-implemented-in-terms-of”建模

1. 什么是组合(composition)? 组合(composition)是一种类型之间的关系,这种关系当一种类型的对象包含另外一种类型的对象时就会产生.举个例子: 1 class Address { ... }; // where someone lives 2 3 class PhoneNumber { ... }; 4 class Person { 5 public: 6 ... 7 private: 8 std::string name; // composed object 9 10

读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎

1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如何用堆资源来表现这个概念的.然而并不是所有资源都是在堆上创建的,对于这种资源,像auto_ptr和tr1::shared_ptr这样的智能指针就不适合当作资源句柄(handle)来使用了.你会发现你时不时的就会需要创建自己的资源管理类. 举个例子,假设你正在使用C API来操纵Mutex类型的互斥信

读书笔记 effective c++ Item 55 让你自己熟悉Boost

你正在寻找一个高质量的,开源的,与平台和编译器无关的程序库的集合?看一下Boost吧.想加入一个由雄心勃勃的,充满天赋的正致力于最高水平的程序库设计和实现工作的C++程序员们组成的团体么?看一下Boost吧.想了解C++将来可能会是什么样子的?看一下Boost吧. Boost是一个C++开发人员组成的团体,也是供免费下载的C++程序库的集合.网址是http://boost.org. 1. Boost的两大优势 当然,有许多C++组织和网站,但是Boost有两点是其它组织不能与之媲美的.首先,它和

读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来

1. 使用模板可能导致代码膨胀 使用模板是节省时间和避免代码重用的很好的方法.你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定的类和300个你需要的函数.(只有在被使用的情况下类模版的成员函数才会被隐式的实例化,所以只有在300个函数被实际用到的情况下才会生成300个成员函数.)函数模板同样吸引人.你不用手动实现许多函数,你只需要实现一个函数模板,然后让编译器来做余下的事情. 然而在有些时候,如果你不小心,使用模板会导致代

读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型

1. 何为public继承的”is-a”关系 在C++面向对象准则中最重要的准则是:public继承意味着“is-a”.记住这个准则. 如果你实现一个类D(derived)public继承自类B(base),你在告诉c++编译器(也在告诉代码阅读者),每个类型D的对象也是一个类型B的对象,反过来说是不对的.你正在诉说B比D表示了一个更为一般的概念,而D比B表现了一个更为特殊的概念.你在主张:任何可以使用类型B的地方,也能使用类型D,因为每个类型D的对象都是类型B的对象:反过来却不对,也就是可以使