Modern Effecive C++ 条款十九:对于共享资源使用std::shared_ptr

    自动管理资源的语言(Java,python)通常内置垃圾回收机制,能够自动识别不再使用的对象并释放它们占用的资源。垃圾回收器负责清理未被引用的对象,所以使用这类语言的程序员不需要手动管理每个对象的生命周期,减少了出错的可能性。虽然方便,但这意味资源的具体回收时间点是不确定的,这可能会影响到性能敏感的应用。

    C++更倾向于拥有对资源生命周期的精细控制,这样可以更好地优化性能和资源利用。通过显式地定义构造函数、析构函数以及使用RAII技术,C++程序可以在特定的时间点准确地释放资源。但是这种级别的控制也增加了代码的复杂度,并且需要程序员更加小心以防止资源泄露等问题。

     为了解决上述两种观点之间的矛盾,C++11引入了std::shared_ptr智能指针。std::shared_ptr结合了自动化管理和可预测销毁的优点:

(1)共享所有权:多个std::shared_ptr可以指向同一个对象,每个std::shared_ptr都持有该对象的所有权。

(2)自动销毁:当最后一个指向某个对象的std::shared_ptr被销毁或重新分配给另一个对象时,该对象就会被自动销毁。

std::shared_ptr的基本机制

引用计数:std::shared_ptr通过维护一个引用计数来跟踪有多少个std::shared_ptr实例指向同一个资源。每当一个新的std::shared_ptr被创建或复制时,该计数递增;当一个std::shared_ptr被销毁或重置为指向其他对象时,该计数递减。一旦引用计数降至0,没有活跃的std::shared_ptr再指向该资源,则会自动调用资源的析构函数并释放内存。

性能考量

大小增加

与普通指针相比,std::shared_ptr通常需要额外的空间来存储引用计数的信息,因此其占用的内存通常是原始指针的两倍左右。std::shared_ptr内部不仅包含了一个指向实际资源的指针,还包含了一个指向引用计数结构的指针。

动态内存分配

引用计数本身是存储在独立于所管理对象的内存块中的。这意味着即使是很小的对象也可能导致一次额外的内存分配操作,以存放这个计数器。这增加内存使用的复杂性和可能的碎片化问题。不过,使用std::make_shared可以优化这一点,因为它允许在一个单独的内存分配中同时创建std::shared_ptr和它所管理的对象,从而减少内存分配次数。

线程安全的引用计数更新

在多线程程序中,多个线程可能同时尝试修改同一个std::shared_ptr对象的引用计数。例如,一个线程可能正在销毁一个std::shared_ptr实例(这会导致引用计数递减),而另一个线程可能正在创建一个新的std::shared_ptr实例指向同一个资源(这会导致引用计数递增)。如果这些操作不是原子性的,那么就可能发生数据竞争,即两个或多个线程同时读取和写入相同的内存位置,导致不确定的行为。为了保证引用计数的一致性和正确性,每次递增或递减操作都必须是原子的,这意味着在任何给定时间点,只有一个线程能够执行这些操作。

shared_ptr移动构造/复制

如果使用移动构造函数或移动赋值运算符从另一个std::shared_ptr中移动资源(而不是复制),则不会增加引用计数。相反,源std::shared_ptr将被设置为nullptr,而新的std::shared_ptr将接管对资源的所有权。移动构造函数和移动赋值运算符使得std::shared_ptr能够更高效地转移所有权,因为它们避免了引用计数的增减操作。这允许std::shared_ptr在某些情况下比使用拷贝构造函数或拷贝赋值运算符更快,尤其是在频繁进行所有权转移的情景下。

shared_ptr 自定义删除器

std::shared_ptr支持自定义删除器,允许用户指定如何释放所管理的对象。与std::unique_ptr不同的是,对于std::shared_ptr来说,删除器类型不是智能指针类型的一部分。这意味着即使两个std::shared_ptr实例拥有不同的删除器,它们仍然可以共享相同的类型,并且可以相互赋值或者一起存储在容器中。

代码展示使用lambda表达式作为删除器:

auto loggingDel = [](Widget *pw) { makeLogEntry(pw); delete pw; };
std::shared_ptr<Widget> spw(new Widget, loggingDel);auto customDeleter1 = [](Widget *pw){…};//自定义删除器,
auto customDeleter2 = [](Widget *pw){…};//每种类型不同
std::shared_ptr<Widget> pw1(new Widget,customDeleter1);
std::shared_ptr<Widget> pw2(new Widget,customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };

shared_ptr 控制块

另一个不同于std::unique_ptr的地方是,指定自定义删除器不会改变std::shared_ptr对象的大小。不管删除器是什么,一个std::shared_ptr对象都是两个指针大小。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。std::shared_ptr怎么能引用一个任意大的删除器而不使用更多的内存?它不能。它必须使用更多的内存。然而那部分内存不是std::shared_ptr对象的一部分。那部分在堆上面,std::shared_ptr创建者利用std::shared_ptr对自定义分配器的支持能力,那部分内存随便在哪都行。前面提到了std::shared_ptr对象包含了所指对象的引用计数的指针有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做控制块control block)。每个std::shared_ptr管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。

控制块的创建规则

  • 使用std::make_shared:总是创建一个新的控制块。这是因为std::make_shared不仅分配内存给对象本身,还为控制块分配内存,并且初始化引用计数。
  • 从独占指针(如std::unique_ptr)构造:创建新的控制块。因为独占指针没有共享的控制块,所以转换成std::shared_ptr时需要创建一个。
  • 从原始指针构造:同样会创建新的控制块。如果有一个已经存在的对象,并希望创建指向它的std::shared_ptr,则应该确保只有一个控制块被创建。如果尝试从同一个原始指针创建多个std::shared_ptr,将会导致每个std::shared_ptr都有自己的控制块,从而导致对象被多次销毁。
auto pw = new Widget;  //创建原始指针
...
std::shared_ptr<Widget> spw1(pw, loggingDel);  // 为*pw创建第一个控制块
...
std::shared_ptr<Widget> spw2(pw, loggingDel); // 为*pw创建第二个控制块

这段代码的问题在于它创建了两个独立的std::shared_ptr,每个都拥有自己的控制块。这将导致当两个std::shared_ptr都离开作用域或被重置时,它们都会尝试删除同一个对象,造成双重释放。

正确的方法

使用std::make_shared:

auto spw1 = std::make_shared<Widget>();  // 使用std::make_shared创建

如果必须使用原始指针和自定义删除器,应直接使用new表达式的结果,而不是存储后的指针变量:

std::shared_ptr<Widget> spw1(new Widget, loggingDel);  // 直接使用new结果

当你需要另一个std::shared_ptr来共享所有权时,通过拷贝现有的std::shared_ptr来避免创建新的控制块:

std::shared_ptr<Widget> spw2(spw1);  //使用spw1的控制块.

在C++中,this指针是一个指向当前对象的指针。如果你尝试将this指针直接传递给std::shared_ptr的构造函数,可能会导致多重所有权问题,尤其是在Widget对象已经被std::shared_ptr管理的情况下。为了避免这种情况,可以使用std::weak_ptr或者确保通过std::shared_ptr来引用对象。

class Widget {
public:void process() {// ...processedWidgets.emplace_back(this);  // 错误地使用this指针}
};std::vector<std::shared_ptr<Widget>> processedWidgets;

问题:this是一个原始指针,而processedWidgets中的元素是std::shared_ptr<Widget>。如果Widget对象已经被std::shared_ptr管理,那么将this传递给emplace_back会创建一个新的std::shared_ptr,这可能导致多重控制块问题(即多个std::shared_ptr管理同一个对象)。

(1)使用std::weak_ptr

#include <memory>
#include <vector>
class Widget {
public:void process(){processedWidgets.emplace_back(std::weak_ptr<Widget>(std::static_pointer_cast<Widget>(self)));
}private:std::weak_ptr<Widget> self;  // 保存一个弱引用
};
std::vector<std::weak_ptr<Widget>> processedWidgets;

self是一个std::weak_ptr<Widget>,它不会增加引用计数。std::weak_ptr用于记录Widget对象,但不会影响对象的生命周期。std::static_pointer_cast用于将self转换为std::shared_ptr<Widget>,以便将其存储在processedWidgets中。

(2)通过std::shared_ptr引用对象

class Widget {
public:void process(std::shared_ptr<Widget> self) {//...processedWidgets.push_back(self);}
};
std::vector<std::shared_ptr<Widget>> processedWidgets;
int main() {auto widget = std::make_shared<Widget>();widget->process(widget);  // 传递self
}

process方法接受一个std::shared_ptr<Widget>参数self。self是对Widget对象的std::shared_ptr引用,这样可以确保processedWidgets中的std::shared_ptr不会引起多重控制块问题。在调用process方法时,传递widget这个std::shared_ptr。

使用enable_shared_from_this

直接使用 this 指针创建新的 std::shared_ptr 可能会导致多重控制块问题,因为每个新创建的 std::shared_ptr 都会尝试创建自己的控制块。通过继承 std::enable_shared_from_this,类可以调用 shared_from_this() 成员函数来获取当前对象的 std::shared_ptr,而不会创建新的控制块。在类的成员函数中,可以通过调用 shared_from_this() 来获取指向当前对象的 std::shared_ptr。shared_from_this() 会查找当前对象已有的控制块,并返回一个新的 std::shared_ptr,该指针与现有控制块关联。调用 shared_from_this() 之前,必须已经有一个 std::shared_ptr 管理着当前对象。如果没有这样的 std::shared_ptr 存在,调用 shared_from_this() 将抛出异常。

私有构造函数和工厂函数:

为了确保对象总是通过 std::shared_ptr 创建,通常将类的构造函数设为私有,并提供一个静态工厂函数来创建并返回 std::shared_ptr。这样可以防止客户端直接创建对象,从而保证对象总是被 std::shared_ptr 管理。

#include <memory>
#include <vector>
class Widget : public std::enable_shared_from_this<Widget> {
public:// 工厂函数,用于创建并返回一个 std::shared_ptr<Widget>template<typename... Ts>static std::shared_ptr<Widget> create(Ts&&... params) {auto widget = std::shared_ptr<Widget>(new Widget(std::forward<Ts>(params)...));return widget;}void process() {// 处理 Widget// 使用 shared_from_this() 获取指向当前对象的 std::shared_ptrprocessedWidgets.emplace_back(shared_from_this());}
private:// 私有构造函数Widget(/* 构造参数 */) {// 初始化}
};
// 存储已处理的 Widget 对象
std::vector<std::shared_ptr<Widget>>processedWidgets;
int main() {// 通过工厂函数创建 Widget 对象auto widget = Widget::create(/* 参数 */);// 调用 process 方法widget->process();// 现在 processedWidgets 中包含了一个指向 widget 的 std::shared_ptrreturn 0;

使用建议

(1)默认配置:使用默认删除器和默认分配器,并通过 std::make_shared 创建 std::shared_ptr 时,控制块的大小和开销都较小。

(2)独占资源:如果不需要共享所有权,使用 std::unique_ptr 更合适,因为它具有接近原始指针的性能,并且可以从 std::unique_ptr 转换为 std::shared_ptr,反之则不行。

(3)std::shared_ptr 通过引用计数自动管理对象的生命周期,提供了强大的共享所有权功能。尽管有控制块、虚函数和原子操作带来的开销,但在大多数情况下,这些开销是可以接受的。对于不需要共享所有权的情况,应优先考虑使用 std::unique_ptr。

(4)使用 std::shared_ptr 管理数组时,应使用 std::shared_ptr<T[]>,而不是尝试用 std::shared_ptr<T> 加自定义删除器来管理数组。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/61006.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从迭代器到生成器:小内存也能处理大数据

有的对象可以用for循环比如字符串和列表,有的对象不可以比如整数 my_str 123 for s in my_str;print(s)my_lst [1,2,3] for i in my_lst:print(i)my_int 123 for n in my_int:print(n) # 报错python中能够使用for循环迭代的对象叫可迭代对象也叫iterables iterables包含__i…

第29天:安全开发-JS应用DOM树加密编码库断点调试逆向分析元素属性操作

时间轴&#xff1a; 演示案例&#xff1a; JS 原生开发-DOM 树-用户交互 DOM&#xff1a;文档操作对象 浏览器提供的一套专门用来操作网页代码内容的功能&#xff0c;实现自主或用户交互动作反馈 安全问题&#xff1a;本身的前端代码通过 DOM 技术实现代码的更新修改&#xff…

“蜀道山”高校联合公益赛 Web (部分)

文章目录 奶龙牌WAF海关警察训练平台恶意代码检测器 奶龙牌WAF <?php if ($_SERVER[REQUEST_METHOD] POST && isset($_FILES[upload_file])) {$file $_FILES[upload_file];if ($file[error] UPLOAD_ERR_OK) {$name isset($_GET[name]) ? $_GET[name] : basen…

docker-compose搭建xxl-job、mysql

docker-compose搭建xxl-job、mysql 1、搭建docker以及docker-compose2、下载xxl-job需要数据库脚本3、创建文件夹以及docker-compose文件4、坑来了5、正确配置6、验证-运行成功 1、搭建docker以及docker-compose 略 2、下载xxl-job需要数据库脚本 下载地址&#xff1a;https…

XML JSON

XML 与 JSON 结构 XML&#xff08;eXtensible Markup Language&#xff09; 1. 定义 XML 是一种标记语言&#xff0c;用于描述数据的结构和内容。主要用于数据存储与交换。 2. 特点 可扩展性&#xff1a;用户可以自定义标签。层次化结构&#xff1a;数据以树形结构组织&…

【Innodb阅读笔记】之 二进制文件应用,主从复制搭建

一、概述 MySQL的主从复制&#xff08;Master-Slave Replication&#xff09;是一种数据复制解决方案&#xff0c;将主数据库的DDL和DML操作通过二进制日志传到从库服务器中&#xff0c;然后在从库上对这些日志重新执行&#xff08;也叫重做&#xff09;&#xff0c;从而是的从…

如何制作项目网页

一、背景 许多论文里经常会有这样一句话Supplementary material can be found at https://hri-eu.github.io/Lami/&#xff0c;这个就是将论文中的内容或者补充视频放到一个网页上&#xff0c;以更好的展示他们的工作。因此&#xff0c;这里介绍下如何使用前人提供的模板制作我…

Spring:Spring事务管理代码案例讲解

Spring事务管理知识讲解请见&#xff1a;Spring事务知识点讲解 下面演示一个代码示例进行理解。 需求 两个账户相互转账&#xff0c;并记录日志&#xff0c;即使有转账失败也要记录 需求分析 这里主要是需要开启事务机制来控制转入和转出&#xff1a; 1&#xff0c;创建一…

了解网络威胁情报:全面概述

网络威胁情报 CTI 是指系统地收集和分析与威胁相关的数据&#xff0c;以提供可操作的见解&#xff0c;从而增强组织的网络安全防御和决策过程。 在数字威胁不断演变的时代&#xff0c;了解网络威胁情报对于组织来说至关重要。复杂网络攻击的兴起凸显了制定强有力的策略以保护敏…

Scrapy图解工作流程-cnblog

1.1 介绍部分&#xff1a; 文字提到常用的Web框架有Django和Flask&#xff0c;接下来将学习一个全球范围内流行的爬虫框架Scrapy。 1.2 内容部分&#xff1a; Scrapy的概念、作用和工作流程 Scrapy的入门使用 Scrapy构造并发送请求 Scrapy模拟登陆 Scrapy管道的使用 Scrapy中…

【ArcGISPro】Sentinel-2数据处理

错误 默认拉进去只组织了4个波段,但是实际有12个波段 解决方案 数据下载 Sentinel-2 数据下载-CSDN博客 数据处理 数据查看 创建镶嵌数据集 在数据管理工具箱中找到创建镶嵌数据集

Python数据分析(OpenCV)

第一步通过pip安装依赖包&#xff0c;执行一下命令 pip install opencv-python 如果是Anaconda请在工具中自行下载 下载好咋们就可以在环境中使用了。 人脸识别的特征数据可以到 github上面下载&#xff0c;直接搜索OpenCV 然后我们在源码中通过cv2的级联分类器引入人脸的特征…

最小生成树-Prim与Kruskal算法

文章目录 什么是最小生成树&#xff1f;Prim算法求最小生成树Python实现&#xff1a; Kruskal算法求最小生成树并查集 Python实现&#xff1a; Reference 什么是最小生成树&#xff1f; 在图论中&#xff0c;树是图的一种&#xff0c;无法构成闭合回路的节点-边连接组合称之为…

深入理解 Java 基本语法之数组

目录 一、数组基础概念 二、数组的声明 1. 基本数据类型数组声明&#xff1a; 2. 引用数据类型数组声明&#xff1a; 三、数组的创建 四、数组的初始化 五、数组的使用 ​编辑1. 获取长度以及访问元素&#xff1a; 2. 数组作为方法的参数&#xff1a; 3. 数组作为方法…

计算机毕业设计PySpark+Scrapy农产品推荐系统 农产品爬虫 农产品商城 农产品大数据 农产品数据分析可视化 PySpark Hadoop

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

17. C++模板(template)1(泛型编程,函数模板,类模板)

⭐本篇重点&#xff1a;泛型编程&#xff0c;函数模板&#xff0c;类模板 ⭐本篇代码&#xff1a;c学习/07.函数模板 橘子真甜/c-learning-of-yzc - 码云 - 开源中国 (gitee.com) 目录 一. 泛型编程 二. 函数模板 2.1 函数模板的格式 2.2 函数模板的简单使用 2.3 函数模板…

Jupyter Notebook的安装和配置提示功能

Python开发环境搭建conda管理环境-CSDN博客 安装anaconda和对接到编译器的教程可以看上面这一篇 Jupyter Notebook是一种交互式计算环境&#xff0c;它允许用户在单个文档中编写和执行代码、方程、可视化和文本。与其他编译器相比&#xff0c;Jupyter Notebook的突出点在于其交…

maxun爬虫工具docker搭建

思路来源开源无代码网络数据提取平台Maxun 先把代码克隆到本地&#xff08;只有第一次需要&#xff09; git clone https://github.com/getmaxun/maxun.git 转到maxun目录 cd maxun 启动容器 docker-compose --env-file .env up -d 成功启动六个容器 网址 http://local…

剑指Offer26.树的子结构

题目让判断B是不是A的子结构 但是我们进行判断是基于 两个树的根相等时, 去判断是否为子结构 针是否等于B的根节点的值对A做先序遍历的过程中 如果根节点相同我们去判断此时B是不是以该根节点的子树的子结构! 实际上进行先序遍历的同时要进行递归判断子结构 B是不是A节点的子结…

2024御网杯信息安全大赛个人赛wp(misc方向)

目录 一.信息安全大赛的通知二、编码转换1. 第一部分2. 第二部分3. 第三部分 三、1.txt四、buletooth 题目附件以及工具链接&#xff1a; 通过网盘分享的文件&#xff1a;御网杯附件 链接: https://pan.baidu.com/s/1LNA6Xz6eZodSV0Io9jGSZg 提取码: jay1 –来自百度网盘超级会…