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

2024-04-07 12:58

本文主要是介绍读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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 use2 3 ...4 5 { // create block to define critical section6 7 Lock ml(&m); // lock the mutex8 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 Mutex6 7 : mutexPtr(pm, unlock) // to point to and the unlock func8 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类的拷贝行为是禁止拷贝,执行引用计数,但其他拷贝行为也是可以实现的。

这篇关于读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/882663

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给