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

简介: 1. 自己实现一个资源管理类  Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如何用堆资源来表现这个概念的。

1. 自己实现一个资源管理类 

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

举个例子,假设你正在使用C API来操纵Mutex类型的互斥信号量对象,来为函数提供lock和unlock:

1 void lock(Mutex *pm); // lock mutex pointed to by pm
2 
3 void unlock(Mutex *pm); // unlock the mutex

为了确保你不会忘记unlock一个已经加过锁的Mutex,你需要创建一个类来管理锁。这样一个类的基本结构已经由RAII准则表述过了,也就是资源会在执行构造的时候获取到,在执行析构的时候释放掉

 1 class Lock {
 2 
 3 public:
 4 
 5 explicit Lock(Mutex *pm)
 6 
 7 : mutexPtr(pm)
 8 
 9 { lock(mutexPtr); } // acquire resource
10 
11 ~Lock() { unlock(mutexPtr); } // release resource
12 
13 private:
14 
15 Mutex *mutexPtr;
16 
17 };

 

客户端以传统的RAII方式来使用锁:

 1 Mutex m; // define the mutex you need to use
 2 
 3 ...
 4 
 5 { // create block to define critical section
 6 
 7 Lock ml(&m); // lock the mutex
 8 
 9 ... // perform critical section operations
10 
11 } // automatically unlock mutex at end
12 
13 // of block

 

2. 对资源管理类进行拷贝会发生什么?

这很好,但如果一个锁对象被拷贝会发生什么呢?

1 Lock ml1(&m); // lock m
2 
3 Lock ml2(ml1); // copy ml1 to ml2 — what should
4 
5 // happen here?

 

上面是一个更加普通的问题,也是每个RAII类的作者必须面对的:当一个RAII对象被拷贝的时候应该发生什么呢?大多数情况下,你将会从下面的4种可能中选择一个:

2.1 禁止拷贝

  • 禁止拷贝。在许多情况下,允许RAII对象被拷贝是没有意义的。对于一个像Lock的类来说这可能是真的,因为一份同步原语(synchronization primitives)的拷贝很少情况下是有意义的。当一个RAII类的拷贝没有意义时,你应该禁止它。Item 6解释了如何可以做到:将拷贝操作声明称private。对于Lock来说,可以是下面这个样子:
1 class Lock: private Uncopyable { // prohibit copying — see
2 
3 public: // Item 6
4 
5 ... // as before
6 
7 };

 

2.2 一份资源,多次引用——使用tr1::shared_ptr

  • 对底层资源进行引用计数。有时候需要保留一个资源直到引用这个资源的最后一个对象被销毁。在这种情况下,拷贝一个RAII对象应该增加对象引用资源的引用计数。这就是用tr1::shared_ptr进行“拷贝”的含义。

 

     通常情况下,RAII类可以通过包含一个tr1::shared_ptr数据成员来实现引用计数的拷贝行为。举个例子,如果Lock想使用引用计数,它可以将mutexPtr的类型从Mutex*改为tr1::shared_ptr<Mutex>。不幸的是,tr1::shared_ptr的默认行为是当引用技术为0的时候会删除它所指向的资源,这不是我们想要的。当我们实现一个Mutex类时,我们只是想unlock,并不想删除它们。幸运的是,tr1::shared_ptr允许指定自己的删除器(”deleter”)---一个函数或者函数对象,引用计数为0的时候会自动调用这个对像。(auto_ptr中不存在这个功能,它总是会删除指针。)这个删除器是tr1::shared_ptr构造函数的第二个可选参数,所以代码会是下面这个样子:

 1 class Lock {
 2 
 3 public:
 4 
 5 explicit Lock(Mutex *pm) // init shared_ptr with the Mutex
 6 
 7 : mutexPtr(pm, unlock) // to point to and the unlock func
 8 
 9 { // as the deleter†
10 
11 lock(mutexPtr.get()); // see Item 15 for info on “get”
12 
13 }
14 
15 private:
16 
17 std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr
18 
19 }; // instead of raw pointer

 

注意在这个例子中,Lock类不再声明析构函数。因为没有必要了。Item 5 解释到一个类的析构函数(无论是编译器生成的还是用户定义的)会自动调用类中的非静态数据成员的析构函数。在这个例子中,非静态数据成员为mutexPtr。但是在mutex的引用计数为0的时候其的析构函数会自动调用tr1::shared_ptr的删除器—也即是unlock。(人们在看到类的源码的时候如果有一行注释来说明你没有忘记析构,你只是使用了编译器默认生成的析构函数,他们会很感激的。)

2.3 一份资源,多次拷贝——深拷贝

  • 拷贝底层的资源。有时你可以拥有一个资源尽可能多的拷贝,你需要一个资源管理类的唯一原因是能够确保资源被使用完毕后能够被释放掉。这种情况下,拷贝一个资源管理对象应该同时拷贝他所包裹(wraps)的资源。也就是拷贝一个资源管理类对象需要执行“深拷贝”。

有一些标准string类型的实现中包含了指向堆内存的指针,组成string的字符会保存在这块内存中。当一个string对象被拷贝的时候,会同时拷贝指针和指针指向的内存。这样的string展示出来的是深拷贝。

2.4 一份资源,一次引用,转移所有权——使用auto_ptr

  • 转移底层资源的所有权。在很少的场合,你可能需要确保只有一个RAII对象指向一个原生(raw)资源,所以当RAII对象被拷贝的时候,资源的拥有权从被拷贝对象转移到了拷贝到的对象。正如Item 13所解释的,这是使用auto_ptr进行拷贝的含义。 

拷贝函数可能由编译器生成,所以除非编译器生成版本能够做到你想要的(Item 5解释了默认版本的行为),否则你需要自己实现它们。一些情况下你可能想支持这些函数的一般版本。这些版本在Item 45进行描述。

3. 总结

  • 拷贝一个RAII对象需要拷贝他所管理的资源,因此资源的拷贝行为决定了RAII对象的拷贝行为。
  • 普通RAII类的拷贝行为是禁止拷贝,执行引用计数,但其他拷贝行为也是可以实现的。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
2月前
|
安全 编译器 程序员
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
48 2
|
6月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
|
5月前
|
机器学习/深度学习 算法 搜索推荐
|
6月前
|
算法 搜索推荐 C++
C++之STL常用算法(遍历、查找、排序、拷贝、替换、算数生成、集合)
C++之STL常用算法(遍历、查找、排序、拷贝、替换、算数生成、集合)
|
6月前
|
编译器 C++ 容器
【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题
【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题
166 0
|
6月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
|
6月前
|
C++ 安全
提高安全性,优雅实现拷贝与交换:C++中的Copy-and-Swap惯用法
拷贝并交换(Copy-and-Swap)是C++中实现赋值操作符和异常安全拷贝构造函数的技巧,涉及使用拷贝构造函数创建临时对象,然后交换数据以确保安全。C++11之前的通用方法,在C++11后可利用移动语义和右值引用优化。示例代码展示了如何运用此方法。
|
4天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
18 2
|
10天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
37 5
|
17天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
47 4