C++智能指针之shared_ptr

2023/9/30 16:47:21

C++智能指针之shared_ptr

  • 前言
  • 一、Shared_ptr
    • 1.1 shared_ptr类的操作
    • 1.2 make_shared函数
    • 1.3 shared_ptr的拷贝赋值
    • 1.4 shared_ptr的自动销毁对象内存机制
    • 1.5 使用动态生存期的资源的类
    • 1.6 shared_ptr与new结合使用
    • 1.7 不要混合使用普通/智能指针
    • 1.8 不要使用 get 初始化另一个智能指针或为智能指针赋值
    • 1.9 reset、unique函数的使用
    • 1.10 异常处理
    • 1.11 使用自己的释放操作
    • 1.12 shared_ptr与动态数组的使用
  • 总结


前言

  在C++中,动态内存的申请和释放是通过运算符:new 和 delete 进行管理的。其中 new 负责申请内存,delete负责释放内存。

  动态内存的使用很容易出现问题,这主要在于你需要保证在正确的时间释放内存,这是比较困难的,如果你忘记释放内存,就会造成内存泄露;有时在还有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。

  为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象,智能指针的行为类似普通指针,最主要的区别在于它负责自动释放所指向的对象。这两种智能指针都定义在 memory 头文件内。

  • 本文是对于shared_ptr的分析。

一、Shared_ptr

  • shared_ptr 类对象默认初始化为一个空指针
  • 类似 vector,智能指针也是模板。所以,创建智能指针必须提供额外的信息,如:
shared_ptr<string> p1;  // shared_ptr,可以指向 string
shared_ptr<list<int>> p2; // shared_ptr,可以指向 int 的 list

  智能指针的使用和普通指针类似,解引用一个智能指针返回它指向的对象,如过在条件判断中使用它,则是检测其是否为空。如:

shared_ptr<string> p1; 
if (p1 && p1->empty()) {
	*p1 = "h1"; // 如果p1指向一个空string,解引用p1,将一个新值赋予 string
}

1.1 shared_ptr类的操作

在这里插入图片描述
在这里插入图片描述

1.2 make_shared函数

  最安全的分配和使用动态内存的方法就是调用make_shared函数,此函数在内存中动态分配对象并初始化它,返回指向此对象的shared_ptr。make_shared 也定义在头文件 memory 中。

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//p2指向一个值为'999999999'的string
shared_ptr<string> p4=make_shared<string>(10, '9');
//p3指向一个值初始化为0的int数
shared_ptr<int> p5 = make_shared<int>();

  类似顺序容器的 emplace 成员,make_shared用其参数来构造给定类型的对象。例如,调用 make_shared时传递的参数必须与 string 的某个构造函数相匹配。

当然,我们通常用auto定义一个对象来保存make_shared的结果:

auto p4 = make_shared<vector<string>>();

1.3 shared_ptr的拷贝赋值

  当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_shared<int>(42); // p指向的对象只有 p 一个引用者
auto q(p); // p和q指向相同对象,此对象有两个引用者

  每个shared_ptr都有一个关联的计数器,通常称其为引用计数。

对shared_ptr类进行拷贝时,计数器就会增加。例如:当用一个shared_ptr初始化另一个shared_ptr、或者它作为参数传递给一个函数以及作为函数的返回值,它所关联的计数器就会增加当我们给让shared_ptr指向另一个对象或者shared_ptr销毁时,原对象的计数器就会递减一旦一个shared_ptr的计数器为0,就会自动释放该对象的内存。

auto r = make_shared<int>(42); // r指向的 int 只有一个引用者
r = q; // 给r赋值,那么: r原来所指的对象引用计数变为0,然后自动释放内存,q所指的对象的引用计数+1

1.4 shared_ptr的自动销毁对象内存机制

  当指向一个对象的最后一个shared_ptr对象被销毁时,shared_ptr类会自动销毁此对象。shared_ptr类是通过析构函数来完成销毁工作的。

shared_ptr还会自动释放相关联的内存

  当动态对象不再使用时,shared_ptr类会自动释放动态对象,这一特性使动态内存的使用变得容易。

  shared_ptr类所指向的内存何时被释放,与shared_ptr类的生存周期有关

  如:我们定义下面的函数返回一个share_ptr指针,指向一个Foo类型的动态分配的对象,对象是通过一个类型为T的参数进行初始化的:

shared_ptr<Foo> factory(T arg) {
    return make_share<Foo>(arg);//返回一个share_ptr类型的智能指针
}

  由于shared_ptr返回一个shared_ptr,所以我们可以确保它分配的对象会在恰当的时刻被释放。如:下面函数调用factory函数来生成一个shared_ptr指针,但是p一旦离开了作用域(use_factory函数),当p销毁时,将递减其引用计数并检查它是否为0,本例中,p是唯一引用 factory 返回的内存的对象。由于p将要销毁,p指向的这个对象也被销毁,因此p所指向的内存地址也就自动释放了。

void use_factory(T arg) {
    shared_ptr<Foo>  p=factory(arg);
}//函数结束之后,p就自动释放它所指向的对象的内存

  下面的函数也是 factory函数来生成一个shared_ptr指针,但是p指针通过返回值返回了,所以,如果有另一个shared_ptr指针调用了该函数,那么该p所指向的内存地址不会随着use_factory函数的调用而释放。

auto use_factory(T arg) {
    shared_ptr<Foo> p=factory(arg);
    return p;
}

1.5 使用动态生存期的资源的类

使用动态内存出于以下三种原因之一:

  • 程序不知道自己需要使用多少对象
  • 程序不知道所需对象的准确类型
  • 程序需要多个对象间共享数据

使用动态内存的一个常见原因是允许多个对象共享相同的状态。

1.6 shared_ptr与new结合使用

  如果我们不初始化一个智能指针,它就会被初始化为一个空指针,我们也能用new返回的指针来初始化一个智能指针,因为,new申请的动态内存的使用、释放等规则仍然符合shared_ptr类的使用规则:

shared_ptr<double> p1; // shared_ptr可以指向一个double
shared_ptr<int> p2(new int(42)); // p2指向一个值为42的int   

  因为智能指针的构造函数是explicit的。因此:我们不能将一个内置指针隐式地转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。

shared_ptr<int> p=new int(1024);   //错误 
shared_ptr<int> p2(new int(1024)); //正确:使用直接初始化

  动态内存作为返回值时的使用手法:限于上面的使用语法,一个返回shared_ptr的函数不能在其返回语句中隐式转换为一个普通指针

shared_ptr<int> clone(int p)
{
    return new int(p); //错误
}
 
shared_ptr<int> clone(int p)
{
    return shared_ptr<int>(new int(p)); //正确
}

  默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。

定义shared_ptr的其他方法:

在这里插入图片描述
在这里插入图片描述

1.7 不要混合使用普通/智能指针

  当一个函数的参数是shared_ptr类时,有以下规则:

  • 函数的调用是传值调用

  • 调用函数时,该shared_ptr类所指向的对象引用计数加1。但是函数调用完成之后,shared_ptr类自动释放,对象的引用计数又减1

void process(shared_ptr<int> ptr){ 
	... // 使用ptr
} // ptr 离开作用域,被销毁
shared_ptr<int> p(new int(42)); //初始化一个智能指针对象p,引用计数+1
process(p);  //p所指的对象引用计数=1,引用计数总数为2
//process函数调用之后,p所指的引用计数减1
int i=*p; //正确

函数参数使用时与new的关系:

  因为shared_ptr类会在生存周期结束之后,将引用计数减1,当引用计数为0时,会释放内存空间
下面是一个特殊的应用场景,需要注意

void process(shared_ptr<int> ptr){ ... }
 
int *x(new int(1024));
process(x);  //错误,不能将int*转换为一个shared_ptr<int>
process(shared_ptr<int>(x)); //合法的,但是process函数返回之后内存会被释放
int j=*x; //错误,x所指的内存已经被释放了

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时被销毁。

1.8 不要使用 get 初始化另一个智能指针或为智能指针赋值

  shared_prt类的get函数返回一个内置指针,指向智能指针所管理的对象

  此函数的设计情况:我们需要向不能使用智能指针的代码传递一个内置指针

  • get函数将内存的访问权限传递给一个指针,但是之后代码不会delete该内存的情况下,对get函数的使用才是最安全的。

  • 永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

shared_ptr<int> p(new int(42));  //引用计数变为1
int *q=p.get();  //正确:使用q需要注意,不要让它管理的指针被释放
 
{//新语句块
    shared_ptr<int>(q); //用q初始化一个智能指针对象
} //语句块结束之后,智能指针对象释放它所指的内存空间
 
int foo=*p;//错误的,p所指的内存已经被释放了

1.9 reset、unique函数的使用

  reset函数会将shared_prt类原先所指的内存对象引用计数减1,并且指向于一块新的内存

shared_ptr<int> p;
 
p=new int(1024);  //错误:不能将一个指针赋予shared_ptr
p=reset(new int(1034)); //正确,p指向一个新对象

  reset函数与unqie函数配合使用:在改变对象之前,检查自己是否为当前对象的唯一用户

shared_ptr<string> p=make_shared<string>("Hello");
 
if(!p.unique()) //p所指向的对象还有别的智能指针所指
    p.reset(new string(*p)); //现在可以放心的改变p了
 
*p+=newVal; //p所指向的对象只有自己一个智能指针,现在可以放心的改变对象的值了

1.10 异常处理

  当程序发生异常时,我们可以捕获异常来将资源被正确的释放。但是如果没有对异常进行处理,则有以下规则:

  shared_ptr的异常处理:如果程序发生异常,并且过早的结束了,那么智能指针也能确保在内存不再需要时将其释放

  new的异常处理:如果释放内存在异常终止之后,那么就造成内存浪费

voif f()
{
    shared_ptr<int> sp(new int(42));
    ...//此时抛出异常,未捕获,函数终止
}//shared_ptr仍然会自动释放内存
voif f()
{
    int *ip=new int(42);
    ...//此时抛出异常,未捕获
    delete ip; //在退出之前释放内存,此语句没有执行到,导致内存浪费
}

1.11 使用自己的释放操作

  当shared_ptr生命周期结束时,会调用默认的析构函数来释放(delete)自己所指向的内存空间。但是我们可以使用shared_prt的语法来指定删除器函数,那么在shared_ptr生命周期结束时就会自动调用这个函数。

示例:

下面是一个shared_ptr指定删除器函数以及避免内存泄露的例子

  错误操作:我们调用f函数来打开一个网络连接,但是在f函数调用之后没有关闭这个连接。因此就会造成内存的泄露

struct destination;    //连接的对象
struct connection;     //连接需要的信息
connection connect(destbination*); //打开连接
void disconnect(connection);       //关闭连接
void f(destination &d)
{
    connection c=connect(&d);//打开一个连接
    ....//使用这个连接
    
    //如果在f函数退出之前忘记调用disconnect函数,那么该连接就没有关闭
}

  正确操作:现在我们定义一个新的函数“end_connection”,并且配合shared_ptr类的使用。shared_ptr指定了一个删除器函数“end_connection”。因此下面的代码能够保证在f函数的各种调用结束时,保证连接正确的关闭

void end_connection(connection *p)
{
    disconnection (*p);
}
 
void f(destination &d)
{
    connection c=connect(&d);
    shared_ptr<connection> p(&c,end_connection);
    ....//使用这个连接
    
    //当f函数退出或者异常退出,p都会调用end_connection函数
}

在这里插入图片描述

1.12 shared_ptr与动态数组的使用

  与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理动态数组,必须提供自己定义的删除器

  如果未提供删除器,shared_ptr默认使用delete删除动态数组,此时delete少一个“[]”,因为会产生错误

//本例中,传递给shared_ptr一个lambda作为删除器
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; } );
shared_ptr<int> sp2(new int[3]{1,2,3}, [](int *p) { delete[] p; });
sp2.reset();  //使用自己书写的lambda释放数组

  动态数组的访问:shared_ptr不支持点和箭头成员运算符访问数组,并且不提供下标运算符访问数组,只能通过get()函数来获取一个内置指针,然后再访问数组元素。

shared_ptr<int> sp(new int[3]{1,2,3}, [](int *p) { delete[] p; });
for (size_t i = 0; i != 3; ++i)
        *(sp.get() + i) = i;

总结

参考:

  • C++ Primer 第五版 P400 - P406
  • C++ Primer 第五版 P412 - P406

期待大家和我交流,留言或者私信,一起学习,一起进步!


http://www.jnnr.cn/a/154094.html

相关文章

世界杯来了,让 Towhee 带你多语言「以文搜球」!

四年一度的世界杯已正式拉开战幕&#xff0c;各小组比赛正如火如荼地进行中。在这样一场球迷的盛宴中&#xff0c;不如让 Towhee 带你「以文搜球」&#xff0c;一览绿茵场上足球战将们的风采吧&#xff5e; 「以文搜球」是跨模态图文检索的一部分&#xff0c;如今最热门的跨模…

百行代码实现VLC简易视频播放器【详细环境配置过程+可执行源码注释完整】

文章目录❓什么是VLC&#x1f680;VLC 库的集成⭐VLC环境配置演示【win10系统vs2017win64】&#x1f34e;VLC 库的基本使用&#x1f382;视频播放器实现⭐自定义函数Unicode2Utf8讲解&#x1f3e0;总结❓什么是VLC VLC 是 Video Lan Client 的缩写&#xff0c;原先是几个法国的…

ES6解析赋值

ES6中新增了一种数据处理方式&#xff0c;可以将数组和对象的值提取出来对变量进行赋值&#xff0c;这个过程时将一个数据结构分解成更小的部分&#xff0c;称之为解析。 1.对象解析赋值: 在ES5中&#xff0c;要将一个对象的属性提取出来&#xff0c;需要经过一下几个过程。 …

springcloud22:sentinal的使用

sentinal对比&#xff08;分布式系统的流量防卫&#xff09; 监控保护微服务 Hystrix 需要自己去手工搭建监控平台&#xff0c;没有一套web界面可以进行细粒度化的配置&#xff0c;流控&#xff0c;速率控制&#xff0c;服务熔断&#xff0c;服务降级… 整合机制&#xff1a;se…

让学前端不再害怕英语单词(四)

|| 欢迎关注csdn前端领域博主: 前端小王hs || email: 337674757qq.com || 前端交流群&#xff1a; 598778642前三章直通车↓↓↓ 让学前端不再害怕英语单词&#xff08;一&#xff09; 让学前端不再害怕英语单词&#xff08;二&#xff09; 让学前端不再害怕英语单词&#xff0…

2022年 SecXOps 安全智能分析技术白皮书 学习笔记 免费下载地址

核心能力 为了加快安全分析能力更全面、更深入的自动化 &#xff0c;SecXOps 的目标在于创建一个集成的用于 Security 的 XOps 实践&#xff0c;提升安全分析的场景覆盖率和运营效率。SecXOps 技术并不 015 SecXOps 技术体系 是 Ops 技术在安全领域的简单加和&#xff0c;SecXO…

C语言练习之递归实现n的k次方

文章目录前言一、思路二、代码以及运行截图1.代码2.运行截图总结前言 使用C语言递归计算N的k次方 一、思路 求n的k次方的原理就是&#xff1a; n^k nn……*n&#xff08;k个n进行相乘&#xff09; 可以得到一个公式&#xff1a; f(k){1k0n∗f(k)k>0f(k) \left\{\begin{…

详解 InnoDB Cluster 主机名问题

详解 InnoDB Cluster 主机名问题 文章目录详解 InnoDB Cluster 主机名问题导言测试过程结论导言 因在写 【InnoDB Cluster】修改已有集群实例名称及成员实例选项 时发现主机名这块有一些问题&#xff0c;在其中进行了部分测试&#xff0c;但为使其内容精简&#xff0c;故将此部…

JSP学习日记

JSP简述 Java Sever Pages----->Java服务器界面 用于前后端结合 jsp为什么淘汰&#xff1f; 由于JSP的前后端耦合性极高&#xff0c;编写代码非常臃肿。前后端的代码放在一起&#xff0c;所以JSP可以看成是已经被淘汰的技术。 为什么还要学jsp&#xff1f; 由于一些公司…

【机器学习项目实战10例】(三):基于K近邻的葡萄酒质量检测项目

💥 项目专栏:【机器学习项目实战10例】 文章目录 一、基于K近邻的葡萄酒质量检测项目二、数据集介绍三、导包四、读取数据五、绘制空间分布六、划分训练集、测试集七、构建K近邻模型八、绘制聚类效果九、网格搜索一、基于K近邻的葡萄酒质量检测项目 葡萄酒数据集是一个经典…

APP逆向案例之(三)sign 参数破解

说明&#xff1a;某新闻APP sign 参数 抓包发现包含内容&#xff1a; url: https://124.*.*.*/api/categorynews/lists 参数&#xff1a; 其中 sign 参数是需要变化的否则访问失败&#xff0c;其余都是固定的 page: 3, size: 10, category: -2, from: -1, lng: 116.363…

Date对象

文章目录Date日期对象Date对象的创建格式化日期3.获取Date总的毫秒数(时间戳)&#xff0c;是距离1970年1月1日过了多少毫秒数。二&#xff1a;常用时间获取方法三&#xff1a;日期设置方法四&#xff1a;时间转字符串菜鸟工具&#xff1a;https://www.runoob.com/jsref/jsref-o…

有监督学习神经网络的回归拟合——基于红外光谱的汽油辛烷值预测(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

Java#27(Arrays)

目录 一.Arrays 操作数组的工具类 二.Lambda表达式 1.注意: 2.省略规则 一.Arrays 操作数组的工具类 方法名 作用 public static String toString(数组) 把数组拼接…

代码随想录65——额外题目【二叉树】:129求根节点到叶节点数字之和、1382将二叉搜索树变平衡、100相同的树、116填充每个节点的下一个右侧节点指针

文章目录1.129求根节点到叶节点数字之和1.1.题目1.2.解答2.1382将二叉搜索树变平衡2.1.题目2.2.解答3.100相同的树3.1.题目3.2.解答4.116填充每个节点的下一个右侧节点指针4.1.题目4.2.解答4.2.1.递归解法4.2.2.迭代方法1.129求根节点到叶节点数字之和 参考&#xff1a;代码随…

机器学习笔记之贝叶斯线性回归(一)线性回归背景介绍

机器学习笔记之贝叶斯线性回归——线性回归背景介绍引言回顾&#xff1a;线性回归场景构建从概率密度函数认识最小二乘法回顾&#xff1a;最小二乘估计回顾&#xff1a;线性回归与正则化关于线性回归的简单小结贝叶斯线性回归贝叶斯方法贝叶斯方法在线性回归中的任务贝叶斯线性…

cocos creator实现浏览星球的功能,附源码

预览效果&#xff1a; 技术要点&#xff1a; 主摄像机的视场轴需要设置为水平。在场景下创建一个空节点用于挂载控制器脚本图片已进行各概念的说明 在“collisionNodeArray”属性下&#xff0c;放置需要点击的星球节点&#xff0c;系统会自己绑定碰撞器。 也可自己提前绑定。 布…

现代密码学导论-16-选择明文攻击和CPA安全

目录 PCA不可区分实验 DEFINITION 3.21 PCA安全的加密方案 LR预言机实验 DEFINITION 3.22 多明文PCA安全的加密方案 THEOREM 3.23 定义3.21和定义3.22等价 PCA不可区分实验 通过运行G(1^n)获得密钥k敌手A被给定输入1^n并拥有访问预言机Enck()的权利&#xff0c;敌手A输出一…

Day12--自定义组件-渲染my-search组件的基本结构

1.自定义搜索组件 我的操作&#xff1a; 1》在uni_modules中右键新建uni_modules插件&#xff1a; 2》看看效果图&#xff1a; ************************************************************************************************************** 2.在分类页面的 UI 结构中&…

Linux进程管理【进程的相关介绍片、ps、 kill 、pstree】【详细整理】

目录进程相关介绍显示系统执行的流程 psps 详解![请添加图片描述](https://img-blog.csdnimg.cn/cd9f10bf36684b419f2f94068afb9a03.png)案例终止进程kill 和 killall基本语法常见选型案例查看进程数pstreepstree [选项]&#xff0c;可以更加直观的来查看进程信息进程相关介绍 …
最新文章