第14条:留心资源管理类中的复制行为

来源:互联网 发布:废钞令 知乎 编辑:程序博客网 时间:2024/05/17 22:16
第 13 条 中介绍了“资源获取即初始化”( Resource Acquisition Is Initialization ,简称 RAII )的概念,它是资源管理的主要内容。同时第 13 条 中 还使用 auto_ptr 和 tr1::shared_ptr 为示例,描述了这一概念是如何管理堆上的资源。然而并不是所有的资源都分配于堆上,对于不分配于堆上的资源,类似于 auto_ptr 和 tr1::shared_ptr 这一类的智能指针并不适合于处理它们。既然如此,有时需要创建自己的资源管理类。举例说,你正使用一个 C 语言的 API 所提供 的 lock 和 unlock 函 数 来处理 Mutex 类型的互斥对象:
void lock(Mutex *pm);             // 通过 pm 为互斥量上锁void unlock(Mutex *pm);           // 为互斥量解锁

为了确保你曾上锁的互斥量都得到解锁,你应该自己编写一个类来管理互斥锁。这样的类的基本结构应遵 循 RAII 的 原理,那就是:资源在构造过程中获得,在析构过程中释放:
class Lock {public:  explicit Lock(Mutex *pm)  : mutexPtr(pm)  { lock(mutexPtr); }           // 获取资源  ~Lock() { unlock(mutexPtr); } // 释放资源private:  Mutex *mutexPtr;};


客户端程序员通过传统 的 RAII 风 格来使用 Lock 类:
Mutex m;       // 定义互斥量以便使用...{              // 创建程序块用来定义临界区Lock ml(&m);  // 为互斥量上锁...            // 进行临界区操作}              // 在程序块末尾互斥量将自动解锁


这样可以正常工作,但是如果复制一个 Lock 对象,将会发生些什么呢?

Lock ml1(&m);  // 锁定mLock ml2(ml1);  // 把 ml1 复制给 ml2 将会发生什么呢? 

有一个问题是所有 的 RAII 类创建者必须面对的:当复制一个 RAII 对象时需要做些什么呢?以上是这个一般化问题的一个较具体的示例。大多情况下你会选择如下可能。
 1、禁止复制。在许多情况下,允许 RAII 被复制没有任何意义。比如对于 Lock 类来说就是这样,因为复制同步原型在大多数情况下都没有什么意义。当复制一个 RAII 类无意义时,你就应该禁止它。第 6 条中详细介绍了实现方法:将拷贝赋值运算符声明为私有的。对于 Lock 而言应该是这样:

class Lock: private Uncopyable {   // 防止复制 — 参见第 6 条public:...     // 同上};

2、为基础资源进行引用计数。有时我们希望保有资源,直到它的最后一个使用者被销毁。在这种情况下复制RAII 对象时应将该资源的“被引用数递增”。这就是 tr1::shared_ptr 所使用的“复制”的含义。
 
通常情况下, RAII 类可以通过包含一个 tr1::shared_ptr 数据成员来实现引用计数复制行为。举例说,如果 Lock 曾希望使用引用计数,它可能会将 mutexPtr 的类型从 Mutex* 更改为 tr1::shared_ptr<Mutex> 。但是不幸的是, tr1::shared_ptr 默认的行为是:当引用计数值变为零时,删除其所指向的内容,但这不是我们想要的。当一个 Mutex 用完时,我们希望对其进行的操作是解锁,而不是删除它。
 
幸运的是tr1::shared_ptr 允许定义一个“删除器”,它是一个函数或一个函数对象,在引用计数值为零时调用。( auto_ptr 并不包含这一特性,它总是删除它所指向的内容。)删除器可作为 tr1::shared_ptr 构造函数的另一个可选参数,所以代码应该是这样的:

class Lock {public:  explicit Lock(Mutex *pm)     // 初始化 shared_ptr ,参数为  : mutexPtr(pm, unlock)       // 所指向的互斥量和解锁函数   {lock(mutexPtr.get());} }    // 关于 "get" 的信息请参见第 15 条  }private:  std::tr1::shared_ptr<Mutex>mutexPtr;};   // 使用 shared_ptr 而不是裸指针

请注意 本例中Lock类不再声明析构函数。因为并不需要。第 5 条中介绍了一个类的析构函数(无论是编译器自动生成的还是用户自定义的)会自动为类的非静态数据成员进行析构。本示例中 mutexPtr 就将被自动析构。 当互斥量的引用计数变为零时, 析构函数会析构 mutexPtr ,然而此时实际上 将会调用 tr1::shared_ptr 的删除器 unlock 。(通常你应该为这个类的代码添加一段注释,你并没有忘记编写析构函数,而是把工作留给了编译器自动生成的默认析构函数。)
 
3、复制主要的资源。一些时候,你可以在需要的情况下为资源复制出任意份数的副本,此时你需要一个资源管理类的唯一理由就是:确保每份副本在其工作完成之后得到释放。在这种情况下,复制资源管理对象的同时,也要复制出其涉及的资源。也可以说,复制资源管理对象时,将进行“深度拷贝”。
 
标准 string 类型的一些实现中,包含着一个指向堆内存的指针,这个指针所指向的就是字符串所保存的位置。这样的 string 对象包含着一个指向堆内存的指针。当一个 string 对象被复制完成之后,指针和其所指内存都会被制作出一个复本。这样的 string 就进行了一次深度复制。
 
4、传递主要资源的所有权。在少数情况下,你可能需要确保仅仅有一个 RAII 对象引用了一个未定义类型的资源,当复制这一 RAII 对象时,资源的所有权也从源对象传递到目标对象了。如同第 13 条中所解释的,这是通过 auto_ptr 所实现的“复制”的含义。
 
拷贝函数(包括拷贝构造函数和拷贝赋值运算符)可能由编译器自动生成,因此除非编译器自动生成版本无法满足你的需要(第 5 条中解释了 C++ 中的默认行为),你就应该自己编写它们。某些情况下你可能还会需要这些函数的一般版本。这将于第 45 条中介绍。
 
需要记住的:
1、复制RAII 对象的同时也要复制其所管理的资源,所以资源的复制行为决定RAII对象的复制行为。
2、一般的 RAII 类在复制时应遵循两条原则:抑制复制,施行引用计数法(reference counting)。不过其他行为也可能被实现。
0 0
原创粉丝点击