# Item 13: 使用对象管理资源
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
假设我们和一个投资(例如,股票,债券等)模型库一起工作,各种各样的投资形式从一个根类 Investment 派生出来:
```
class Investment { ... }; // root class of hierarchy of
// investment types
```
进一步假设这个库使用了通过一个 factory 函数(参见 Item 7)为我们提供特定 Investment 对象的方法:
```
Investment* createInvestment(); // return ptr to dynamically allocated
// object in the Investment hierarchy;
// the caller must delete it
// (parameters omitted for simplicity)
```
通过注释指出,当 createInvestment 函数返回的对象不再使用时,由 createInvestment 的调用者负责删除它。那么,请考虑,写一个函数 f 来履行以下职责:
```
void f()
{
Investment *pInv = createInvestment(); // call factory function
... // use pInv
delete pInv; // release object
}
```
这个看上去没问题,但是有几种情形会造成 f 在删除它从 createInvestment 得到的 investment 对象时失败。有可能在这个函数的 "..." 部分的某处有一个提前出现的 return 语句。如果这样一个 return 执行了,控制流程就再也无法到达 delete 语句。还可能发生的一个类似情况是如果 createInvestment 的使用和删除在一个循环里,而这个循环以一个 continue 或 goto 语句提前退出。还有,"..." 中的一些语句可能抛出一个异常。如果这样,控制流程不会再到达那个 delete。无论那个 delete 被如何跳过,我们泄漏的不仅仅是容纳 investment 对象的内存,还包括那个对象持有的任何资源。
当然,小心谨慎地编程能防止这各种错误,但考虑到这些代码可能会随着时间的流逝而发生变化。为了对软件进行维护,一些人可能会在没有完全把握对这个函数的资源管理策略的其它部分的影响的情况下增加一个 return 或 continue 语句。尤有甚者,f 的 "..." 部分可能调用了一个从不惯于抛出异常的函数,但是在它被“改良”后突然这样做了。依赖于 f 总能到达它的 delete 语句根本靠不住。
为了确保 createInvestment 返回的资源总能被释放,我们需要将那些资源放入一个类中,这个类的析构函数在控制流程离开 f 的时候会自动释放资源。实际上,这只是本 Item 介绍的观念的一半:将资源放到一个对象的内部,我们可以依赖 C++ 的自动地调用析构函数来确保资源被释放。(过一会儿我们还要介绍本 Item 观念的另一半。)
许多资源都是动态分配到堆上的,并在一个单独的块或函数内使用,而且应该在控制流程离开那个块或函数的时候释放。标准库的 auto_ptr 正是为这种情形量体裁衣的。auto_ptr 是一个类似指针的对象(一个智能指针),它的析构函数自动在它指向的东西上调用 delete。下面就是如何使用 auto_ptr 来预防 f 的潜在的资源泄漏:
```
void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); // call factory
// function
... // use pInv as
// before
} // automatically
// delete pInv via
// auto_ptr's dtor
```
这个简单的例子示范了使用对象管理资源的两个重要的方面:
* 获得资源后应该立即移交给资源管理对象。如上,createInvestment 返回的资源被用来初始化即将用来管理它的 auto_ptr。实际上,因为获取一个资源并在同一个语句中初始化资源管理对象是如此常见,所以使用对象管理资源的观念也常常被称为 Resource Acquisition Is Initialization (RAII)。有时被获取的资源是被赋值给资源管理对象的,而不是初始化它们,但这两种方法都是在获取资源的同时就立即将它移交给资源管理对象。
* 资源管理对象使用它们的析构函数确保资源被释放。因为当一个对象被销毁时(例如,当一个对象离开其活动范围)会自动调用析构函数,无论控制流程是怎样离开一个块的,资源都会被正确释放。如果释放资源的动作会引起异常抛出,事情就会变得棘手,不过,关于那些问题请访问 Item 8,所以我们不必担心它。
因为当一个 auto_ptr 被销毁的时候,会自动删除它所指向的东西,所以不要让超过一个的 auto_ptr 指向同一个对象非常重要。如果发生了这种事情,那个对象就会被删除超过一次,而且会让你的程序通过捷径进入未定义行为。为了防止这个问题,auto_ptrs 具有不同寻常的特性:拷贝它们(通过拷贝构造函数或者拷贝赋值运算符)就是将它们置为空,拷贝的指针被设想为资源的唯一所有权。
```
sstd::auto_ptr<Investment> // pInv1 points to the
pInv1(createInvestment()); // object returned from
// createInvestment
std::auto_ptr<Investment> pInv2(pInv1); // pInv2 now points to the
// object; pInv1 is now null
pInv1 = pInv2; // now pInv1 points to the
// object, and pInv2 is null
```
这个奇怪的拷贝行为,增加了潜在的需求,就是通过 auto_ptrs 管理的资源必须绝对没有超过一个 auto_ptr 指向它们,这也就意味着 auto_ptrs 不是管理所有动态分配资源的最好方法。例如,STL 容器要求其内含物能表现出“正常的”拷贝行为,所以 auto_ptrs 的容器是不被允许的。
相对于 auto_ptrs,另一个可选方案是一个引用计数智能指针(reference-counting smart pointer, RCSP)。一个 RCSP 是一个智能指针,它能持续跟踪有多少对象指向一个特定的资源,并能够在不再有任何东西指向那个资源的时候删除它。就这一点而论,RCSP 提供的行为类似于垃圾收集(garbage collection)。与垃圾收集不同的是,无论如何,RCSP 不能打破循环引用(例如,两个没有其它使用者的对象互相指向对方)。
TR1 的 tr1::shared_ptr(参见 Item 54)是一个 RCSP,所以你可以这样写 f:
```
void f()
{
...
std::tr1::shared_ptr<Investment>
pInv(createInvestment()); // call factory function
... // use pInv as before
} // automatically delete
// pInv via shared_ptr's dtor
```
这里的代码看上去和使用 auto_ptr 的几乎相同,但是拷贝 shared_ptrs 的行为却自然得多:
```
void f()
{
...
std::tr1::shared_ptr<Investment> // pInv1 points to the
pInv1(createInvestment()); // object returned from
// createInvestment
std::tr1::shared_ptr<Investment> // both
pInv1 and pInv2 now
pInv2(pInv1); // point to the object
pInv1 = pInv2; // ditto — nothing has
// changed
...
} // pInv1 and pInv2 are
// destroyed, and the
// object they point to is
// automatically deleted
```
因为拷贝 tr1::shared_ptrs 的工作“符合预期”,它们能被用于 STL 容器以及其它和 auto_ptr 的非正统的拷贝行为不相容的环境中。
不要搞错,本 Item 不是关于 auto_ptr,tr1::shared_ptr 或任何其它种类的智能指针。而是关于使用对象管理资源的重要性的。auto_ptr 和 tr1::shared_ptr 仅仅是做这些事的对象的例子。(关于 tr1::shared_ptr 的更多信息,请参考 Item 14,18 和 54。)
auto_ptr 和 tr1::shared_ptr 都在它们的析构函数中使用 delete,而不是 delete []。(Item 16 描述两者的差异。)这就意味着将 auto_ptr 或 tr1::shared_ptr 用于动态分配的数组是个馊主意,可是,可悲的是,那居然可以编译:
```
std::auto_ptr<std::string> // bad idea! the wrong
aps(new std::string[10]); // delete form will be used
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 两个类提供你在寻找的行为。
本 Item 的关于使用对象管理资源的指导间接表明:如果你手动释放资源(例如,使用 delete,而不使用资源管理类),你就是在自找麻烦。像 auto_ptr 和 tr1::shared_ptr 这样的预制的资源管理类通常会使本 Item 的建议变得容易,但有时,你使用了一个资源,而这些预加工的类不能如你所愿地做事。如果碰上这种情况,你就需要精心打造你自己的资源管理类。那也并非困难得可怕,但它包含一些需要你细心考虑的微妙之处。那些需要考虑的事项是 Item 14 和 15 的主题。
作为最后的意见,我必须指出 createInvestment 的裸指针(raw pointer)的返回形式就是资源泄漏的请帖,因为调用者忘记在他们取回来的指针上调用 delete 实在是太容易了。(即使他们使用一个 auto_ptr 或 tr1::shared_ptr 来完成 delete,他们仍然必须记住将 createInvestment 的返回值存储到智能指针对象中。)对付这个问题需要改变 createInvestment 的接口,这是我在 Item 18 中安排的主题。
Things to Remember
* 为了防止资源泄漏,使用 RAII 对象,在 RAII 对象的构造函数中获得资源并在析构函数中释放它们。
* 两个通用的 RAII 是 tr1::shared_ptr 和 auto_ptr。tr1::shared_ptr 通常是更好的选择,因为它的拷贝时的行为是符合直觉的。拷贝一个 auto_ptr 是将它置为空。
- Preface(前言)
- Introduction(导言)
- Terminology(术语)
- Item 1: 将 C++ 视为 federation of languages(语言联合体)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 确保 objects(对象)在使用前被初始化
- Item 5: 了解 C++ 为你偷偷地加上和调用了什么函数
- Item 6: 如果你不想使用 compiler-generated functions(编译器生成函数),就明确拒绝
- Item 7: 在 polymorphic base classes(多态基类)中将 destructors(析构函数)声明为 virtual(虚拟)
- Item 8: 防止因为 exceptions(异常)而离开 destructors(析构函数)
- Item 9: 绝不要在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)
- Item 10: 让 assignment operators(赋值运算符)返回一个 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中处理 assignment to self(自赋值)
- Item 12: 拷贝一个对象的所有组成部分
- Item 13: 使用对象管理资源
- Item 14: 谨慎考虑资源管理类的拷贝行为
- Item 15: 在资源管理类中准备访问裸资源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一个独立的语句中将 new 出来的对象存入智能指针
- Item 18: 使接口易于正确使用,而难以错误使用
- Item 19: 视类设计为类型设计
- Item 20: 用 pass-by-reference-to-const(传引用给 const)取代 pass-by-value(传值)
- Item 21: 当你必须返回一个对象时不要试图返回一个引用
- Item 22: 将数据成员声明为 private
- Item 23: 用非成员非友元函数取代成员函数
- Item 24: 当类型转换应该用于所有参数时,声明为非成员函数
- Item 25: 考虑支持不抛异常的 swap
- Item 26: 只要有可能就推迟变量定义
- Item 27: 将强制转型减到最少
- Item 28: 避免返回对象内部构件的“句柄”
- Item 29: 争取异常安全(exception-safe)的代码
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之间的编译依赖
- Item 32: 确保 public inheritance 模拟 "is-a"
- Item 33: 避免覆盖(hiding)“通过继承得到的名字”
- Item 34: 区分 inheritance of interface(接口继承)和 inheritance of implementation(实现继承)
- Item 35: 考虑可选的 virtual functions(虚拟函数)的替代方法
- Item 36: 绝不要重定义一个 inherited non-virtual function(通过继承得到的非虚拟函数)
- Item 37: 绝不要重定义一个函数的 inherited default parameter value(通过继承得到的缺省参数值)
- Item 38: 通过 composition(复合)模拟 "has-a"(有一个)或 "is-implemented-in-terms-of"(是根据……实现的)
- Item 39: 谨慎使用 private inheritance(私有继承)
- Item 40: 谨慎使用 multiple inheritance(多继承)
- Item 41: 理解 implicit interfaces(隐式接口)和 compile-time polymorphism(编译期多态)
- Item 42: 理解 typename 的两个含义
- Item 43: 了解如何访问 templatized base classes(模板化基类)中的名字
- Item 44: 从 templates(模板)中分离出 parameter-independent(参数无关)的代码
- Item 45: 用 member function templates(成员函数模板) 接受 "all compatible types"(“所有兼容类型”)
- Item 46: 需要 type conversions(类型转换)时在 templates(模板)内定义 non-member functions(非成员函数)
- Item 47: 为类型信息使用 traits classes(特征类)
- Item 48: 感受 template metaprogramming(模板元编程)
- Item 49: 了解 new-handler 的行为
- Item 50: 领会何时替换 new 和 delete 才有意义
- Item 51: 编写 new 和 delete 时要遵守惯例
- Item 52: 如果编写了 placement new,就要编写 placement delete
- 附录 A. 超越 Effective C++
- 附录 B. 第二和第三版之间的 Item 映射