Fork me on GitHub

读书笔记 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中进行讨论。

posted @ 2017-02-18 07:56  HarlanC  阅读(902)  评论(0编辑  收藏  举报