【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都不会得到释放。
所以在没有使用智能指针的情况下,这种问题还是很难解决的。
我们顺便回顾一下什么是内存泄漏。
内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费
如果长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终会卡死。这种造成的后果还是非常的严重的。
要避免出现内存泄漏,就要做到:
- 在前期做好良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个理想状态。如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用
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是如何解决的呢?解决办法也比较简单粗暴,那就是不让拷贝或者赋值,也就是防拷贝
如何防拷贝呢?
- 只声明不实现
- 限定为私有
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++的独特之处。但是我们的学习还远没有结束,下一节我们会讲解类的设计模式中的常用的模式之一:单例模式。希望大家可以持续关注。
相关文章:

【C++进阶】RAII思想&智能指针
智能指针 一,为什么要用智能指针(内存泄漏问题)内存泄漏 二,智能指针的原理2.1 RAII思想2.2 C智能指针发展历史 三,更靠谱的shared_ptr3.1 引用计数3.2 循环引用3.3 定制删除器 四,总结 上一节我们在讲抛异…...
探索量子计算:打开未来技术的大门
在科技领域,每一次技术革命都能开启新的可能性,推动人类社会进入一个新的时代。当前,量子计算作为一种前沿技术,正引领着下一轮科技革命的浪潮。本文将深入探索量子计算的奥秘,解析其工作原理,并通过一个简…...

C++11 设计模式2. 简单工厂模式
简单工厂(Simple Factory)模式 我们从实际例子出发,来看在什么情况下,应用简单工厂模式。 还是以一个游戏举例 //策划:亡灵类怪物,元素类怪物,机械类怪物:都有生命值࿰…...

RabbitMQ-死信队列常见用法
目录 一、什么是死信 二、什么是死信队列 编辑 三、第一种情景:消息被拒绝时 四、第二种场景:. 消费者发生异常,超过重试次数 。 其实spring框架调用的就是 basicNack 五、第三种场景: 消息的Expiration 过期时长或队列TTL…...

2024/4/14周报
文章目录 摘要Abstract文献阅读题目创新点CROSSFORMER架构跨尺度嵌入层(CEL)CROSSFORMER BLOCK长短距离注意(LSDA)动态位置偏置(DPB) 实验 深度学习CrossFormer背景维度分段嵌入(DSW)…...

MySQL 社区版 安装总结
很早就安装过MySQL,没有遇到过什么问题,直接next就行了,这次在新电脑上安装却遇到了一些问题,记录一下。 安装的是MySQL社区版,下载地址是www.mysql.com,进入后选择DOWNLOAD页面,选择MySQL Com…...
二叉排序树的增删改查(java版)
文章目录 1. 基本节点2. 二叉排序树2.1 增加节点2.2 查找(就是遍历)就一起写了吧2.3 广度优先遍历2.4 删除(这个有点意思)2.5 测试样例 最后的删除,目前我测试的是正确的 1. 基本节点 TreeNode: class TreeNode{pri…...
linux下coredump问题的定位分析方法
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 参考:https://blog.csdn.net/m0_73698480/article/details/130077852 最近定位了一段时间linux下的崩溃问题,又收集了一些思路,特整理记录一下。 常见coredump定位方法是:…...

第十届蓝桥杯省赛真题(C/C++大学B组)
目录 试题 A: 组队 试题 B: 年号字串 试题 C: 数列求值 试题 D: 数的分解 试题 E: 迷宫 试题 F: 特别数的和 试题 G:完全二叉树的权值 试题 H:等差数列 试题 I:后缀表达式(不一定对) 试题 J:灵能…...

Scrapy 爬取m3u8视频
Scrapy 爬取m3u8视频 【一】效果展示 爬取ts文件样式 合成的MP4文件 【二】分析m3u8文件路径 视频地址:[在线播放我独自升级 第03集 - 高清资源](https://www.physkan.com/ph/175552-8-3.html) 【1】找到m3u8文件 这里任务目标很明确 就是找m3u8文件 打开浏览器…...
LVGL简单记录
1、 vs中代码旁边有个小锁删除git 2、Visual Studio 试图编译已删除的文件, 如果这个文件也是你不再需要编译的文件,且已经从文件系统中删除,你需要从 .vcxproj 文件中移除或者注释掉这一行,以停止Visual Studio尝试去编译一个不…...

计算机网络——ARP协议
前言 本博客是博主用于复习计算机网络的博客,如果疏忽出现错误,还望各位指正。 这篇博客是在B站掌芝士zzs这个UP主的视频的总结,讲的非常好。 可以先去看一篇视频,再来参考这篇笔记(或者说直接偷走)。 …...

【C++]C/C++的内存管理
这篇博客将会带着大家解决以下几个问题 1. C/C内存分布 2. C语言中动态内存管理方式 3. C中动态内存管理 4. operator new与operator delete函数 5. new和delete的实现原理 6. 定位new表达式(placement-new) 1. C/C内存分布 我们先来看下面的一段代码和相关问题 int global…...

深入理解计算机网络分层结构
一、 为什么要分层? 计算机网络分层的主要目的是将复杂的网络通信过程分解为多个相互独立的层次,每个层次负责特定的功能。这样做有以下几个好处: 模块化设计:每个层次都有清晰定义的功能和接口,使得网络系统更易于设…...

亚马逊云科技CTO带你学习云计算降本增效秘诀
2023亚马逊云科技一年一度的重磅春晚--Re:invent上有诸多不同话题的主题Keynote,这次小李哥带大家复盘来自亚马逊CTO: Wener博士的主题演讲: 云架构节俭之道1️⃣节俭对于云计算为什么重要? ▶️企业基础设施投入大,利用好降本策略可以减少巨…...

快速上手Vue
目录 概念 创建实例 插值表达式 Vue响应式特性 概念 Vue是一个用于 构建用户界面 的 渐进式 框架 构建用户界面:基于数据渲染出用户看到的页面 渐进式:Vue相关生态:声明式渲染<组件系统<客户端路由<大规模状态管理<构建工具 V…...
java 目录整理
Java知识相关目录主要参考黑马程序员 风清扬老师的视屏,参考链接为 Java_黑马刘意(风清扬)2019最新版_Java入门视频_Java入门_Java编程_Java入门教程_黑马教程_黑马程序员_idea版_哔哩哔哩_bilibili 1、java 基础 java基本认识?java跨平台原理?jdk、jre、jvm的联系? 链接:…...

使用Python的Pillow库进行图像处理书法参赛作品
介绍: 在计算机视觉和图像处理领域,Python是一种强大而流行的编程语言。它提供了许多优秀的库和工具,使得图像处理任务变得轻松和高效。本文将介绍如何使用Python的wxPython和Pillow库来选择JPEG图像文件,并对选中的图像进行调整和…...
docker 容器指定utf-8编码
在运行 Docker 容器的时候,如果容器内应用需要使用 UTF-8 编码来正常处理中文,你可以通过设置环境变量来指定编码。 可以使用 -e 或者 --env 标志来设置环境变量。比如,设置 LANG 和 LC_ALL 环境变量为 C.UTF-8 或者 en_US.UTF-8:…...

单例模式以及常见的两种实现模式
单例模式是校招中最常考的设计模式之一. 设计模式其实就是类似于“规章制度”,按照这个套路来进行操作。 单例模式能保证某个类在程序中只存在唯一 一份实例。而不会创建出多个实例,如果创建出了多个实例,就会编译报错。而不会创建出多个实…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...

el-switch文字内置
el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...
【git】把本地更改提交远程新分支feature_g
创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...

C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...

排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...