【C++】share_ptr详解

一、share_ptr 的简单使用

1.1、基本用法

从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。

智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。

智能指针的行为类似于一个常规指针,与常规指针之间重要的区别就是它负责自动释放所管理的资源,share_ptr 使用引用计数,允许多个 share_ptr 指向同一资源,每多一个 share_ptr 指向该资源,share_ptr 的引用计数就 +1 ,减为0时表示没有 share_ptr 对该资源进行引用了,就会释放所指向的资源。share_ptr 内部中的引用计数是线程安全的,但是引用的资源不是线程安全的。

1.2、初始化 

  1. 裸指针直接初始化,但不能通过隐式转换来构造,因为 share_ptr 构造函数被声明为 explicit
  2. 允许移动构造与拷贝构造
  3. 通过 make_share 构造
#include <iostream>
#include <memory>class test {};int main()
{std::shared_ptr<test> f(new test());              // 裸指针直接初始化//std::shared_ptr<test> f1 = new test();            // Error,explicit禁止隐式初始化std::shared_ptr<test> f2(f);                       // 拷贝构造函数std::shared_ptr<test> f3 = f;                      // 拷贝构造函数f2 = f;                                             // copy赋值运算符重载std::cout << f3.use_count() << " " << f3.unique() << std::endl;std::shared_ptr<test> f4(std::move(new test()));        // 移动构造函数//std::shared_ptr<test> f5 = std::move(new test());       // Error,explicit禁止隐式初始化std::shared_ptr<test> f6(std::move(f4));                 // 移动构造函数std::shared_ptr<test> f7 = std::move(f6);                // 移动构造函数std::cout << f7.use_count() << " " << f7.unique() << std::endl;std::shared_ptr<test[]> f8(new test[10]());             // Error,管理动态数组时,需要指定删除器std::shared_ptr<test> f9(new test[10](), std::default_delete<test[]>());auto f10 = std::make_shared<test>();               // std::make_shared来创建return 0;
}

在初始化上 share_ptr 与 unique_ptr 在初始化上的方式就有区别,二者都不支持隐式初始化,但是 unque_ptr 不支持拷贝构造和拷贝赋值,而 share_ptr 则都支持。

1.3、删除器

删除器可以是普通函数、函数对象和 lambda 表达式等,默认的删除器为 std::default_delete ,内部是使用 delete 来实现的,与 unique_ptr 不同,删除器不是 share_ptr 类型的组成部分,也就是说两个 sptr1,sptr2 有不同的删除器,但只要它们的类型是相同的都可以被放入同一容器中。此外,在动态管理数组的时候,share_ptr 需要指定删除器。智能指针的大小与被引用的资源的大小是无关的,因为智能指针是也是通过指针来对该资源进行访问,而不是存储在智能指针内部。

#include <iostream>
#include <memory>
#include <vector>class Frame {};int main()
{auto del1 = [](Frame* f){std::cout << "delete1" << std::endl;delete f;};auto del2 = [](Frame* f){std::cout << "delete2" << std::endl;delete f;};std::shared_ptr<Frame> f1(new Frame(), del1);std::shared_ptr<Frame> f2(new Frame(), del2);std::unique_ptr<Frame, decltype(del)> f3(new Frame(), del);std::vector<std::shared_ptr<Frame> > v;v.push_back(f1);v.push_back(f2);return 0;
}

二、剖析 share_ptr

2.1、share_ptr 的内存模型

share_ptr 的内存模型长这样(直接使用的陈硕大神的图)

将其简化只剩下引用计数与原始指针后长这样:

从图中我们可以看出,share_ptr 包含了一个指向对象的指针和一个指向控制块的指针。每一个由share_ptr 管理的对象都有一个控制块,除了包含强引用计数、弱引用技术之外,还包含了自定义删除器的副本和分配器的副本以及其他附加数据。

2.2、控制块的创建规则

  1. std::make_shared 总是创建一个控制块
  2. 从具备所有权的指针触发构造一个 share_ptr 的时候,会创建一个控制块(比如 unique_ptr 在转换成 share_ptr 的时候会创建控制块,因为 unique_ptr 本身不使用控制块,同时 unique_ptr 置空)
  3. 当 share_ptr 构造函数使用裸指针作为实参时,会创建一个控制块(也就是说,从同一个裸指针出发构造多个 share_ptr 的时候会创建多个控制块,这也就意味着裸指针可能会被释放多次。如果想从一个已经拥有控制块的对象出发创建一个 share_ptr ,此时我们就可以传递一个 share_ptr 或 weak_ptr 而非裸指针作为构造函数的参数,这样就不会创建新的控制器)

因此,尽可能避免将裸指针传递给 share_ptr 的一个有效的办法是 make_shared。如果必须将一个裸指针作为参数传入到 share_ptr 的构造函数,就直接传递 new 运算符运算的结果而非传递一个裸指针。

2.3、尽量使用 make 函数

  1. make_shared 内部是通过调用 allocate_shared 来进行实现的,与 new 相比,make 系列函数的优势:
  2. 避免代码冗余,创建智能指针的时候,被创建的对象只需要写一次,如 make_shared,而用 new 来进行创建智能指针的时候需要写两次。
  3. 异常安全性,make 系列函数可编写异常安全代码,增强了安全性

使用 make 函数与使用 new 进行构造的 share_ptr 内存布局如下:

make 系列函数的局限性:

  1. 所有的 make 系列函数都不允许自定义删除器
  2. make 系列函数创建对象时,不能接受{}初始化列表(这是因为完美转发的转发函数是一个模板函数,利用模板类型进行推导。因此无法把{}推导为 initializer_list)也就是说,make 系列只能将圆括号内的参数进行转发
  3. 自定义内存管理的类(如重载了 operator new 和 operator delete)不建议使用 make_shared 来创建,因为重载 operator new 和 operator delete 时,往往用来分配和释放该类精确尺寸的内存块,而 make_shared 创建的 shared_ptr,是一个自定义了分配器 (allocate_shared) 和删除器的智能指针,由 allocate_shared 分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小
  4. 对象的内存可能无法及时回收。因为:make_shared 只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由 shared_ptr 和 weak_ptr 共享的,因此两者共同管理着这个内存块(托管对象 + 控制块)。当强引用计数为 0 时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个 weak_ptr 离开作用域时,弱引用也减为 0 才会释放这块内存块。原本强引用减为 0 时就可以释放的内存, 现在变为了强引用和弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。

2.4、引用计数

  1. shared_ptr 中的引用计数直接关系到何时是否进行对象的析构,因此它的变动变得尤为重要
  2. shared_ptr 的构造函数会使该引用计数递增,而析构函数则会使得引用计数递减。但移动构造表示从一个已有的 shared_ptr 移动构造到另一个新的 shared_ptr ,这意味着一旦新的 shared_ptr 产生后,原有的 shared_ptr 就会被置空,结果就是引用计数没有变化。
  3. 拷贝赋值构造同时执行两种操作如(例如 p1 和 p2 是指向不同对象的 shared_ptr ,则执行         p1 = p1 时,将修改 p1 使得其指向 p2 所值的对象,而最初 p1 所指向的对象的引用计数递减,同时 p2 所指向的对象引用计数递增 )
  4. reset 函数,如果不带参数时,则引用计数减 1。如果带参数时,如 sp.reset(p) 则 sp 原来指向的对象引用计数减 1 ,同时 sp 指向新的对象 p。
  5. 如果实施一次递减后,最后的引用计数变为 0 ,即不再有 shared_ptr 指向该对象,则会被 shared_ptr 析构掉。

需要注意的是:引用计数本身是安全且无锁的,但对象的读写则不是。

2.5、this 返回 shared_ptr 

不要将 this 指针返回给 shared_ptr。当希望将 this 指针托管给 shared_ptr 时,类需要继承自          std::enable_shared_from_this ,并且从 shared_from_this() 中获得 shared_ptr 指针。

#include <iostream>
#include <memory>class Frame {public:std::shared_ptr<Frame> GetThis() {return std::shared_ptr<Frame>(this);}
};int main()
{std::shared_ptr<Frame> f1(new Frame());std::shared_ptr<Frame> f2 = f1->GetThis();std::cout << f1.use_count() << " " << f2.use_count() << std::endl;std::shared_ptr<Frame> f3(new Frame());std::shared_ptr<Frame> f4 = f3;std::cout << f3.use_count() << " " << f4.use_count() << std::endl;return 0;
}

运行结果:

此时就很奇怪,GetThis() 返回的不是 f1 本身嘛?而为什么 f2 和 f4 的结果就有这样的区别呢?

其实,直接从 this 指针创建,会为 this 对象创建新的控制块,也就相当于从裸指针重新创建一个新的控制块。

为了解决这个问题,标准库提供了解决方法:让类派生于一个模板类:enable_shared_from_this<T>,然后调用 shared_from_this 函数即可

通过调用 shared_from_this 成员函数获得一个和this指针指向相同对象的shared_ptr。

class Frame : public std::enable_shared_from_this<Frame> {public:std::shared_ptr<Frame> GetThis() {return shared_from_this();}
};

原理:

template<typename _Tp>
class enable_shared_from_this
{protected:constexpr enable_shared_from_this() noexcept { }enable_shared_from_this(const enable_shared_from_this&) noexcept { }enable_shared_from_this& operator=(const enable_shared_from_this&) noexcept { return *this; }~enable_shared_from_this() { }public:shared_ptr<_Tp> shared_from_this(){ return shared_ptr<_Tp>(this->_M_weak_this); }shared_ptr<const _Tp> shared_from_this() const{ return shared_ptr<const _Tp>(this->_M_weak_this); }private:template<typename _Tp1>void _M_weak_assign(_Tp1* __p, const __shared_count<>& __n) const noexcept{ _M_weak_this._M_assign(__p, __n); }template<typename _Tp1, typename _Tp2>friend void __enable_shared_from_this_helper(const __shared_count<>&,const enable_shared_from_this<_Tp1>*,const _Tp2*) noexcept;mutable weak_ptr<_Tp>  _M_weak_this;
};

可以看到 enable_shared_from_this 模板类提供两个 public 属性的 shared_from_this 成员函数。这两个成员函数内部会通过 _M_weak_this 成员来创建 shared_ptr 。其中 _M_weak_assign 函数不能手动调用,这个函数会被 shared_ptr 自动调用,用处是来初始化唯一的成员变量 _M_weak_this 。

分析:根据对象生成顺序,先初始化基类 enable_shared_from_this ,再初始化派生类 Frame 对象本身,这时 Frame 对象已经生成,但 _M_weak_this 成员还未被初始化,最后应通过 shared_ptr<T> sp(new T()) 等方式调用 shared_ptr 构造函数(内部会调用 _M_weak_assign 成员函数来初始化 _M_weak_this 成员)。而如果在调用 shared_from_this 函数之前 weak_this_ 成员未被初始化,则会通过 ASSERT 报错显示。

更深层次:这个 enable_shared_from_this 中有一个弱指针 weak_ptr ,这个弱指针能够监视 this 指针,在调用 shared_from_this 这个函数时,这个函数内部实际上是调用 weak_ptr 的 lock 方法,lock() 会让 shared_ptr 指针计数 +1,同时返回这个 shared_ptr。

参考:

第21课 shared_ptr共享型智能指针 - 浅墨浓香 - 博客园 (cnblogs.com)

【C++】shared_ptr共享型智能指针详解-CSDN博客

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

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

相关文章

【MySQL数据库】数据类型和简单的增删改查

目录 数据库 MySQL的常用数据类型 1.数值类型&#xff1a; 2.字符串类型 3.日期类型 MySQL简单的增删改查 1.插入数据&#xff1a; 2.查询数据&#xff1a; 3.修改语句&#xff1a; 4.删除语句&#xff1a; 数据库 平时我们使用的操作系统都把数据存储在文件中&#…

深入了解服务器硬件:从基础知识到实际应用

在当今数字化的社会中&#xff0c;服务器扮演着至关重要的角色&#xff0c;它们是支撑互联网、云计算、大数据等技术发展的基石。而理解服务器硬件的基础知识对于从事IT领域的人员来说至关重要。本文将从服务器硬件的基础知识出发&#xff0c;介绍服务器硬件的组成、作用及其在…

Python算法100例-4.3 多项式之和

完整源代码项目地址&#xff0c;关注博主私信源代码后可获取 1.问题描述2.问题分析3.算法设计4.确定程序框架5.完整的程序 1&#xff0e;问题描述 计算下列多项式的值&#xff1a; 2&#xff0e;问题分析 方法一&#xff1a;把上面多项式中的每一个分项标上记号&#xff0c…

浅谈C++引用的使用以及底层原理

1、引用概念 引用不是新定义一个变量&#xff0c;而 是给已存在变量取了一个别名&#xff0c;编译器不会为引用变量开辟内存空间&#xff0c;它和它引用的变量共用同一块内存空间。 类型& 引用变量名(对象名) 引用实体&#xff1b; 注意&#xff1a;引用类型必须和引用实体…

华清远见作业第五十三天——ARM(第七天)

代码 key_inc.h #ifndef __KEY_INC_H__ #define __KEY_INC_H__ #include "stm32mp1xx_gic.h" #include "stm32mp1xx_exti.h" #include "stm32mp1xx_rcc.h" #include "stm32mp1xx_gpio.h"void key1_it_config();void key2_it_config(…

【polarctf的部分题解】

【web】phar —》私有属性赋值 当时遇到不知道privated该怎样赋值才可以&#xff0c;链子挺简单的&#xff0c;但是语法不熟悉 <?php include funs.php; highlight_file(__FILE__); if (isset($_GET[file])) {if (myWaf($_GET[file])) {include($_GET[file]);} else {unse…

案例分析:汽车零配件行业CRM解决方案,成功案例揭秘!

近年来&#xff0c;国家大力推动新能源汽车行业发展&#xff0c;在国内汽车工业实现“弯道超车”的同时&#xff0c;新能源汽车出口海外&#xff0c;新市场有望为自主新能源方向贡献增量。 汽车行业的快速发展&#xff0c;势必会带动汽车零配件行业走向增长。从政策方面看&…

牛客网BC-33 统计成绩(数组排序思想)

题目如下 --------------------------------------------------------------------------------------------------------------------------------- 思路&#xff1a;以数组形式输入&#xff0c;并将数组顺序&#xff08;或者逆序&#xff09;排序&#xff0c;最后输出最大值最…

Redis入门到实战-第十二弹

Redis实战热身Bitfields篇 完整命令参考官网 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://redis.io/Redis概述 Redis是一个开源的&#xff08;采用BSD许可证&#xff09;&#xff0c;用作数据库、缓存、消息代理…

并发编程之Callable、Runnable、Future与FutureTask

目录 前言一、Callable与Runnable1.1 Callable1.2 Runnable1.3 二者对比 二、Future与FutureTask2.1 Future2.2 FutureTask2.3 二者对比 三、综合使用3.1 Callable执行Future获取结果3.2 Callable执行任务FutureTask获取执行结果 四、应用场景 前言 在 Java 中&#xff0c;Cal…

小明SEO:网站域名被投诉怎么恢复呢?分享

小明SEO对他的网站进行了分析&#xff0c;发现网站上存在大量非法内容&#xff0c;比如股票、金融、外汇等&#xff0c;甚至还有虚假宣传来吸引其他网站的流量。 随后他检查了该网站的tdk设置&#xff0c;尤其是网站标题&#xff0c;发现也存在违规行为。 这就是网站域名被投诉…

React Native 应用打包

引言 在将React Native应用上架至App Store时&#xff0c;除了通常的上架流程外&#xff0c;还需考虑一些额外的优化策略。本文将介绍如何通过配置App Transport Security、Release Scheme和启动屏优化技巧来提升React Native应用的上架质量和用户体验。 配置 App Transport…

Python 构建项目工具库之pybuilder使用详解

概要 在Python项目开发中,良好的构建和自动化流程是非常重要的。PyBuilder是一个用于构建Python项目的工具,它提供了简单易用的方式来定义和管理项目的构建过程,包括依赖管理、测试、代码质量检查等。本文将深入探讨PyBuilder库的使用方法、功能特性以及如何利用它来构建优…

SQL映射文件

一、SQL映射的xml文件 1.1 mapper元素 二、select 三、别名与Java映射 四、resultMap 啊

一道很有意思的题目(考初始化)

这题很有意思&#xff0c;需要你对初始化够了解才能解出来 &#xff0c;现在我们来看一下吧。 这题通过分析得出考的是初始化。关于初始化有以下知识点 &#xff08;取自继承与多态&#xff08;继承部分&#xff09;这文章中&#xff09; 所以根据上方那段知识点可知&#xf…

Linux/Backdoor

Backdoor Enumeration nmap 第一次扫描发现系统对外开放了22&#xff0c;80和1337端口&#xff0c;端口详细信息如下 22端口对应的是ssh服务&#xff0c;80端口使用Apache&#xff0c;title上写着backdoor&#xff0c;而且可以看出使用了wordpress&#xff0c;1337端口暂时还…

HyperWorks2023 下载地址及安装教程

HyperWorks是一套由Altair Engineering开发的集成化仿真平台。这个平台涵盖了许多不同领域的仿真和优化应用&#xff0c;包括结构分析、流体力学、多体动力学、优化、电磁场分析等。 HyperWorks提供了一系列强大的工具和模块&#xff0c;用于进行复杂的工程仿真和优化任务。它…

数据结构 之 队列习题 力扣oj(附加思路版)

优先级队列 #include<queue> --队列 和 优先级队列的头文件 优先级队列&#xff1a; 堆结构 最大堆 和 最小堆 相关函数&#xff1a; front() 获取第一个元素 back() 获取最后一个元素 push() 放入元素 pop() 弹出第一个元素 size() 计算队列中元素…

简单了解synchronized

什么是synchronized synchronized是Java提供的一个关键字&#xff0c;用于方法或者代码块&#xff0c;保证并发安全。 synchronized使用场景 同步代码块&#xff08;原子性&#xff09; synchronized可以用在方法上&#xff0c;或者用在代码块。 可锁的对象可以是普通对象…

【ZZULI数据结构实验一】多项式的三则运算

【ZZULI数据结构实验一】多项式的四则运算 ♋ 结构设计♋ 方法声明♋ 方法实现&#x1f407; 定义一个多项式类型并初始化---CreateDataList&#x1f407; 增加节点---Getnewnode&#x1f407; 打印多项式类型的数据-- PrintPoly&#x1f407; 单链表的尾插--Listpush_back&…