【C++进阶】RAII思想&智能指针

智能指针

  • 一,为什么要用智能指针(内存泄漏问题)
    • 内存泄漏
  • 二,智能指针的原理
    • 2.1 RAII思想
    • 2.2 C++智能指针发展历史
  • 三,更靠谱的shared_ptr
    • 3.1 引用计数
    • 3.2 循环引用
    • 3.3 定制删除器
  • 四,总结

上一节我们在讲抛异常时,就引出了利用智能指针来防止出现内存泄漏的问题,现在我们来看一下智能指针。

一,为什么要用智能指针(内存泄漏问题)

接着上一节的问题,如果我们在捕获异常时刚好在堆上new了一段空间,如果我们没有重新抛出异常,那么在堆上的空间该如何释放呢?

先来看一下这段代码:

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

我们知道new本身也会抛异常,如果p1这里抛异常或者p2这里抛异常,都会导致p1或者p2得不到释放。
如果在div中抛异常,那么p1和p2都不会得到释放。

所以在没有使用智能指针的情况下,这种问题还是很难解决的。

我们顺便回顾一下什么是内存泄漏。

内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费

如果长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终会卡死。这种造成的后果还是非常的严重的。

要避免出现内存泄漏,就要做到:

  1. 在前期做好良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个理想状态。如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。

二,智能指针的原理

2.1 RAII思想

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做法有两大好处:
1. 不需要显式地释放资源,可以有效防止抛异常所导致的问题
2. 采用这种方式,对象所需的资源在其生命期内始终保持有效

智能指针就是这种思想的应用,这里我们先来简单实现一个智能指针,智能指针当然也是一种指针,所以也要重载->*
也就是利用smart_ptr这个类的生命周期来控制资源

template<class T>
class smart_ptr {
public:smart_ptr(T* ptr):_ptr(ptr){}~smart_ptr() {delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{smart_ptr <int> sp1(new int);smart_ptr <int> sp2(new int);cout << div() << endl;
}int main()
{try {Func();}catch(const exception& e){cout<<e.what()<<endl;}return 0;
}

当在main函数中捕获到异常后,会跳出Func这个函数的作用域,那么随之这两个smart_ptr 也会跟着销毁,同时在析构函数中将sp1和sp2跟着销毁。

但是这里又会有新的问题,如果要拷贝这个智能指针呢?

	smart_ptr <int> sp1(new int);smart_ptr <int> sp2 = sp1;

这里就会造成这两个sp1和sp2管理的是同一块资源,出了这个作用域后会销毁,这时就会造成两次析构的问题。

那么要如何解决这个问题呢?我们就要从C++智能指针的发展来看看了。

2.2 C++智能指针发展历史

1.C++第一个智能指针是在C++98提出来的,就是auto_ptr
在这里插入图片描述

但是auto_ptr有一个很大的问题就是,其实现的时候用的是管理权转移的思想,即:

auto_ptr<int> sp1(new int);
auto_ptr<int> sp2(sp1); // 管理权转移

sp1拷贝给sp2后,sp1就会失效,也就是说不论拷贝还是赋值后,原先的智能指针把对这块资源的管理权全部交给了被拷贝或者赋值的那个智能指针,然后会将原先的智能指针置为空。

这样就会很坑了,所以在很多使用智能指针的场景下,auto_ptr是很明令禁止不能使用的。


2.C++11又推出了新版本的智能指针,unique_ptr

在这里插入图片描述
那么unique_ptr是如何解决的呢?解决办法也比较简单粗暴,那就是不让拷贝或者赋值,也就是防拷贝
如何防拷贝呢?

  1. 只声明不实现
  2. 限定为私有
unique_ptr<int> sp1(new int);
unique_ptr<int> sp2(sp1);

这里可以看到是不可以进行拷贝的

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


3.如果就是要拷贝呢?
C++11之后又开始提供更靠谱的并且支持拷贝的shared_ptr

shared_ptr是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。也就是当指向某个资源的个数为1时才释放。

我们具体在下面来进行讲解并且模拟实现一下:

三,更靠谱的shared_ptr

3.1 引用计数

shared_ptr是用引用计数的思想来保证可以去拷贝和赋值的。也就是shared_ptr在其内部,给每个指向的资源都维护了着一份计数,用来记录该份资源被几个对象共享
在这里插入图片描述
在这里插入图片描述
如果某个shared_ptr的引用计数是0,就说明自己是最后一个使用该资源的对象,就必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

赋值也是一样的,赋值后也要将对用的引用计数进行改变
在这里插入图片描述

下面是模拟实现的代码:

template<class T>
class shared_ptr {
public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}void release(){if (--(*_pcount) == 0){//cout << "delete->" << _ptr << endl;//delete _ptr;delete _pcount;}}~shared_ptr() {release();}//拷贝构造shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}//赋值shared_ptr operator=(const shared_ptr<T>& sp) {//这里要分情况(如果被拷贝的对象引用计数只有一次就要释放)if (_ptr != sp._ptr) {//这里直接判断其指向的是不是同一个资源,可以解决直接或者间接自己给自己赋值的情况release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}
private:T* _ptr;int* _pcount;
};

注意:这里在重载=时要处理一下自己给自己赋值的情况

3.2 循环引用

C++11出现的shared_ptr虽然解决了拷贝的问题,但是又引出了新的问题,就是循环引用的问题

我们来结合下面的场景来分析一下循环引用
看下面的代码:

struct ListNode
{int val;shared_ptr<ListNode> next;shared_ptr<ListNode> prev;~ListNode(){cout << "~ListNode()" << endl;}
};int main(){shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);n1->next = n2;n2->prev = n1;return 0;
}

运行后我们看到n1和n2并没有被释放。但是如果我们只要屏蔽掉n1->next=n2或者n2->prev = n1,n1和n2都会释放。
没有释放那就是内存泄漏!!

这是为什么呢?

在这里插入图片描述

那么这样的问题要如何解决呢?

这里就要用到weak_ptr了,weak_ptr是专门用来解决循环引用的问题的,他不是RAII思想,不会增加引用计数。
这里的解决办法就是将ListNode里的next和prev用weak_ptr代替

weak_ptr<ListNode> next;
weak_ptr<ListNode> prev;

这里可以看到weak_ptr是支持用shared_ptr去赋值的
在这里插入图片描述
这里我们简单实现了weak_ptr,想看的大家可以进入我的gitee查看:智能指针

3.3 定制删除器

这里还有一个小问题就是,如果我们使用智能指针所控制的这个空间不是new出来的而是new [],或者malloc出来的,那么我们在使用时就会出问题,因为shared_ptr在内部是delete。

shared_ptr<ListNode> sp1(new ListNode[10]);

这里就要用到定制删除器,其实就是传入一个仿函数(lambda表达式也可以)去删除指定类型。

template<class T>
struct DeleArry {void operator()(T* ptr) {delete[] ptr;}
};shared_ptr<ListNode> sp1(new ListNode[10],DeleArry<ListNode>());

或者lambda表达式

shared_ptr<ListNode> sp3(new ListNode[10], [](ListNode* ptr) { delete[] ptr; });

当然相应的shared_ptr的结构也要做相应的变化,感兴趣大家可以自己简单模拟实现一下。
大家也可以来看看我模拟实现的:智能指针

四,总结

这里我们也就讲完了智能指针了,智能指针还是很实用的。到这里我们C++的大部分重难点也就结束了。如果你到了这里,首先恭喜你坚持学习到了现在并且感受到了C++的独特之处。但是我们的学习还远没有结束,下一节我们会讲解类的设计模式中的常用的模式之一:单例模式。希望大家可以持续关注。

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

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

相关文章

PostgreSQL入门到实战-第二十九弹

PostgreSQL入门到实战 PostgreSQL中数据分组操作(四)官网地址PostgreSQL概述PostgreSQL中CUBE命令理论PostgreSQL中CUBE命令实战更新计划 PostgreSQL中数据分组操作(四) 如何使用PostgreSQL CUBE生成多个分组集 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不…

InternlM2

第一次作业 基础作业 进阶作业 1. hugging face下载 2. 部署 首先&#xff0c;从github上git clone仓库 https://github.com/InternLM/InternLM-XComposer.git然后里面的指引安装环境

【Golang学习笔记】从零开始搭建一个Web框架(二)

文章目录 模块化路由前缀树路由 前情提示&#xff1a; 【Golang学习笔记】从零开始搭建一个Web框架&#xff08;一&#xff09;-CSDN博客 模块化路由 路由在kilon.go文件中导致路由和引擎交织在一起&#xff0c;如果要实现路由功能的拓展增强&#xff0c;那将会非常麻烦&…

[尚硅谷flink] 检查点笔记

在Flink中&#xff0c;有一套完整的容错机制来保证故障后的恢复&#xff0c;其中最重要的就是检查点。 文章目录 11.1 检查点11.1.1 检查点的保存1&#xff09;周期性的触发保存2&#xff09;保存的时间点3&#xff09;保存的具体流程 11.1.2 从检查点恢复状态11.1.3 检查点算法…

【python】 numpy 中常用随机数函数的使用和记忆(不易混淆)

文章目录 概述固定分布随机数&#xff08;只需指定形状的随机函数&#xff09;np.random.randomnp.random.randnp.random.randn 随机范围随机数&#xff08;需要指定范围和形状的随机函数&#xff09;np.random.randintnp.random.uniformnp.random.normalnp.random.poisson 代码…

乐写9612手写板实测故障

闲鱼上淘了二手的 ①需要驱动很强的usb口&#xff0c;老usb口会不识别&#xff0c;尤其是笔记本容易不识别&#xff0c;非常容易出现下面这种问题&#xff1a; ②需要microsoft2013以上的&#xff0c;兼容性做的比较差 ③由于可视化&#xff0c;导致数据线容易烧&#xff0c;…

超标量处理器设计:基于竞争的分支预测分支预测的更新

★ 继续学习体系结构的知识。 接着上一讲继续写 ★上一讲&#xff1a;超标量处理器设计&#xff1a;基于全局历史的分支预测-CSDN博客 ★上上一讲&#xff1a;超标量处理器设计&#xff1a;两位饱和计数器&基于局部历史的分支预测-CSDN博客 知识回顾&#xff1a; 基于局部…

策略模式【行为模式C++】

1.概述 策略模式是一种行为设计模式&#xff0c; 它能让你定义一系列算法&#xff0c; 并将每种算法分别放入独立的类中&#xff0c; 以使算法的对象能够相互替换。 策略模式通常应用于需要多种算法进行操作的场景&#xff0c;如排序、搜索、数据压缩等。在这些情况下&#x…

将公式用MathType转换导入LaTex的方法 Mathtype+laTex 混合使用 在mathtype中打latex代码编辑公式

关于LaTex和Word之间孰优孰劣的争论已经持续了很长时间&#xff0c;有些朋友在写论文时还会纠结是用Word好还是用LaTex好&#xff0c;其实他们两也是各有优势&#xff0c;LaTeX 与 Word 是两种不同类型的文本编辑处理系统。Latex是无格式的明文文档&#xff0c;格式保存在源文件…

dbeaver数据库语言编辑器设置jdbc驱动

打开 dbeaver 软件 数据库 -> 驱动管理器 以mysql为例 双击 MySQL -> 库 -> 添加工件 然后 打开maven组件库 官网 找到mysql驱动对应的maven工件地址 复制进去然后确认就行了 参考 大神博客

stm32f103c8t6hal库使用看门狗

STM32F103C8T6是一款性能强大的微控制器&#xff0c;它具有丰富的外设和功能&#xff0c;其中之一就是看门狗&#xff08;Watchdog&#xff09;。看门狗是一种重要的硬件设备&#xff0c;它可以在系统出现异常时自动重启系统&#xff0c;以保证系统的稳定运行。我们将详细介绍如…

001vscode为什么设置不了中文?

VSCode中文插件安装 在VSCode中设置中文的首要步骤是安装“Chinese (Simplified) Language Pack for Visual Studio Code”扩展插件。这一过程十分简单&#xff0c;只需打开VSCode&#xff0c;进入扩展市场&#xff0c;搜索“ Chinese (Simplified) Language Pack ”然后点击…

transformer

通过5个条件判定一件事情是否会发生&#xff0c;5个条件对这件事情是否发生的影响力不同&#xff0c;计算每个条件对这件事情发生的影响力多大&#xff0c;写一个transformer模型pytorch程序,最后打印5个条件分别的影响力。 示例一 为了计算每个条件对一件事情发生的影响力&am…

会声会影2024全新视频剪辑软件

会声会影 (CyberLink PowerDirector) 是一款既强大又易于使用的视频编辑软件&#xff0c;具有众多功能和工具&#xff0c;专为用户设计制作高质量的视频剪辑和制作。无论您是一个新手或是一个有经验的电影制作者&#xff0c;会声会影都适合您的需要。 生活即电影 软件下载地址&…

JAVA面试八股文之数据库

MySQL面试题 MySQL 存储引擎架构了解吗&#xff1f;CHAR 和 VARCHAR 的区别是什么&#xff1f;索引是越多越好嘛&#xff1f;MySQL数据库中空值&#xff08;null&#xff09;和空字符串&#xff08;&#xff09;的区别&#xff1f;SQL 中 on 条件与 where 条件的区别&#xff1…

【设计模式】六大设计原则

设计原则 研究 23 种设计模式是困难的&#xff0c;甚至是没必要的六大设计原则零、单一职责原则开闭原则里氏代换原则依赖倒置原则接口隔离原则迪米特法则合成复用原则 研究 23 种设计模式是困难的&#xff0c;甚至是没必要的 设计模式有23种&#xff0c;我认为对普通人来说想…

Windows 关闭占用指定端口的进程

以下示例以443端口为例&#xff0c;具体哪个端口视自己情况而定 输入命令 # 输出的最后一列就是进程号pid netstat -ano | findstr "443" 找出占用443端口的进程号(pid)&#xff08;第二列是你本机的应用占用的端口&#xff0c;看第二列就行&#xff09;如下图&am…

音频变速python版

音频变速 如何能在不改变音频其他特点的情况下&#xff0c;只改变语速呢&#xff1f; 有几个python的库可以实现该功能&#xff0c;下面一一介绍。 pydub库 首先&#xff0c;确保安装了pydub和ffmpeg。 下面是一个简单的Python脚本&#xff0c;展示如何改变音频的播放速度&a…

OpenCV4.9​​​​基本阈值操作

目标 在本教程中&#xff0c;您将学习如何&#xff1a; 使用 OpenCV 函数 cv&#xff1a;&#xff1a;threshold 执行基本阈值操作 理论依据 注意 下面的解释属于 Bradski 和 Kaehler 的 Learning OpenCV 一书 阈值&#xff1f; 最简单的分割方法应用示例&#xff1a;分…

使用 R.swift(生成不了R.generated.swift)

今天算是正儿八经创建第一个swift工程&#xff0c;照着视频引用R.swift pod R.swift 工程配置 "$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift" $TEMP_DIR/rswift-lastrun $SRCROOT/R.generated.swift * 注意 Run角本要放在 Che…