智能指针(C++)
目录
一、智能指针是什么
二、为什么需要智能指针
三、智能指针的使用和原理
3.1、RALL
3.2 智能指针的原理
3.3、智能指针的分类
3.3.1、auto_ptr
3.3.2、unique_ptr
3.2.4、weak_ptr
一、智能指针是什么
在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。
所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
下面是智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象,构造函数和析构函数。

二、为什么需要智能指针
异常的重新抛出的场景:
void File()
{string filename;cin >> filename;FILE* fout = fopen(filename.c_str(), "r");if (fout == nullptr) {string errmsg = "打开文件失败:";errmsg += filename;errmsg += "->";errmsg += strerror(errno);Exception e(errno, errmsg);throw e;}char ch;while ((ch = fgetc(fout))!=EOF) {cout << ch;}fclose(fout);
}double Division(int a, int b)
{if (b == 0) {string errmsg = "Division by zero condition!";Exception e(100, errmsg);throw e;}else{return ((double)a / (double)b);}
}void Func()
{int* p = new int[100];int len, time;cin >> len >> time;try {cout << Division(len,time) << endl;File();}catch (...){//捕获之后,不是要处理异常,异常由最外层同一处理//这里捕获异常只是为了处理内存泄漏的问题delete[]p;throw; }delete[]p;
}int main()
{try {Func();}catch (const Exception& e) {cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}
在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。
但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。
所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题。
三、智能指针的使用和原理
3.1、RALL
- 不需要显式的释放资源
- 采用这种方式,对象所需的资源在其生命周期内始终保持有效.
我们可以借助RALL思想来写一个简单的 智能指针:
#include<iostream>
using namespace std;template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr =nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;cout<<"~SmartPtr"<<endl;}
private:T* _ptr;
};int main()
{int* a = new int(1);SmartPtr<int> sp(a); //将a 指针委托给sp对象管理SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理
}

3.2 智能指针的原理
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}
private:T* _ptr;
};struct Date
{int _year;int _month;int _day;
};int main()
{SmartPtr<int> sp1(new int);*sp1 = 10;cout<<*sp1<<endl;SmartPtr<int> sparray(new Date);// 需要注意的是这里应该是sparray.operator->()->_year = 2018;// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->sparray->_year = 2018;sparray->_month = 1;sparray->_day = 1;
}
3.3、智能指针的分类
上面的SmartPtr 还不可以被称为智能指针,因为它还不具有指针的行为与性质。
指针可以解引用,也可以通过->去访问所指向的空间中的内容,因此智能指针还需要将 *,->重载。
除此之外,如果我们使用了 拷贝或者赋值操作,就会发生浅拷贝的问题,由于二者指向同一块空间,所以在析构的时候也会析构两次,造成错误。
所以,为了解决以上问题,C++提供了几种设计方案实现的智能指针。
C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景
3.3.1、auto_ptr
在C++98版本的库种,提供了 auto_ptr 的智能指针:
class Date
{
public:Date():_year(0),_month(0),_day(0){}~Date(){}int _year;int _month;int _day;};int main()
{auto_ptr<Date>ap(new Date);//拷贝构造auto_ptr<Date>copy(ap);ap->_year = 2022;
}
我们发现报错了,发生了非法访问。
这就是auto_ptr 的弊病,当我们使用对象拷贝或者赋值之后,之前的那个对象就被置空(如下图)

在拷贝或者赋值的过程种,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。
虽然这种方法确实解决了 浅拷贝的问题,但是十分局限性也很大,这也就导致了,我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。
由于C++98种提供的这个智能指针问题明显,所以在实际工作种哼多公司是明确规定了不能使用auto_ptr的。
auto_ptr具体实现:
template<class T>class auto_ptr{public:auto_ptr(T* ptr=nullptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr; //管理权转移}auto_ptr<T>& operator = (auto_ptr<T>& ap){if (this != *ap) {delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~SmartPtr(){if (_ptr)delete _ptr;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}private:T* _ptr;};
3.3.2、unique_ptr
在C++11中,C++11y引入了unique_ptr.
unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。
对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。
template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//防拷贝unique_ptr(unique_ptr<T>& ap) = delete;unique_ptr<T>& operator = (unique_ptr<T>& ap) = delete;~SmartPtr(){if (_ptr)delete _ptr;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}private:T* _ptr;};
3.3.3、shared_ptr
C++中还提供了shared_ptr。
shared_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。
shared_ptr的原理:
我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
shared_ptr 的模拟实现
template<class T>class shared_ptr{public:shared_ptr(T*ptr =nullptr):_ptr(ptr),_pcount(new int(1)){}//拷贝构造shared_ptr(const T& sp)_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);}//赋值拷贝shared_ptr<T>& operator = (shared_ptr<T>& sp){if (_ptr != sp._ptr) {if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}~shared_ptr(){if (--(*_pcount) == 0 && _ptr) {delete _pcount;delete _ptr;}}private:T* _ptr;int* _pcount;};
我们把 这个 计数器 建在堆上,这样就可以保证各个对象之间保持同步同时计数正确。
拷贝构造

赋值拷贝
赋值拷贝需要注意两点:
- 在被赋值之前的对象需要将自己析构,也就是放弃当前资源的管理权,然后再去被赋值,取得新的管理权。
- 避免自己对自己赋值,按照1中的机制,如果自己对自己赋值,会造成无谓的操作,或者误析构资源。

另一种写法:
shared_ptr<T>& operator=(shared_ptr<T> sp){swap(_ptr, sp._ptr);swap(_pcount, sp._pcount);return *this;}
但是,此时我们的shared_ptr 还面临着 线程安全的问题。
这里我们需要保障的是对于 计数器的 ++ 和 – 造成的线程不安全。对于资源的线程安全问题,这不是智能指针保证的部分
template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}void add_ref(){_pmtx->lock();++(*_pcount);_pmtx->unlock();}void release_ref(){bool flag = false;_pmtx->lock();if (--(*_pcount) == 0 && _ptr) {delete _pcount;delete _ptr;flag = true;cout << "释放资源:" << _ptr << endl;}_pmtx->unlock();if (flag)delete _pmtx;}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx){add_ref();}shared_ptr<T>& operator = (const shared_ptr<T>& sp){if (_ptr != sp._ptr) {if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = sp._ptr;_pcount = sp._pcount;add_ref();}return *this;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}T* get(){return _ptr;}int use_count(){return *_pcount;}~shared_ptr(){release_ref();}private:T* _ptr;int* _pcount;mutex* _pmtx;};
定制删除器
不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。
为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete

这个自定义删除器可以是函数指针,仿函数,lamber,包装器。
仿函数的删除器
shared_ptr中的析构函数会去调用DelArry仿函数去释放动态数组。


3.2.4、weak_ptr
虽然 shared_ptr 确实已经是一个不错的设计了,但是没有“十全十美”的东西,在一些特别的场景之下shared_ptr 也无能为力:
shared_ptr 的循环引用
我们看下面的场景,我们运行发现,两个节点n1.n2 都没有析构。



在出了作用域之后,首先把 n1,n2 两个对象析构,此时两边计数器均减为1,那么左边节点资源什么时候析构呢, 当n2->prev析构,也就是当右边节点资源析构,那么右边节点资源什么时候析构呢,当n1->_next析构,也就是当左边节点资源析构…我们发现,此时形成了一个类似于“死锁”的情况。
此时我们就要使用 weak_ptr 来解决 循环引用
weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的,为什么这么说呢,我们可以看看它的构造函数:
我们只能使用 wek_ptr或者 shared_ptr 去初始化它。

我们在会产生循环引用的位置,把shared_ptr换成weak_ptr。 weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。

实现一个weak_ptr
template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(shared_ptr<T>& sp):_ptr(sp.get()),_pcount(sp.use_count()){}weak_ptr(weak_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){}weak_ptr& operator = (shared_ptr<T>& sp){_ptr = sp.get();_pcount = sp.use_count();return *this;}weak_ptr& operator = (weak_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;int* _pcount;};
相关文章:
智能指针(C++)
目录 一、智能指针是什么 二、为什么需要智能指针 三、智能指针的使用和原理 3.1、RALL 3.2 智能指针的原理 3.3、智能指针的分类 3.3.1、auto_ptr 3.3.2、unique_ptr 3.3.3、shared_ptr 3.2.4、weak_ptr 一、智能指针是什么 在c中,动态内存的管理式通过一…...
社区店商业模式探讨:如何创新并持续盈利?
在竞争激烈的商业环境中,社区店要想获得成功并持续盈利,需要不断创新和优化商业模式。 作为一名开鲜奶吧5年的创业者,我将分享一些关于社区店商业模式创新的干货和见解,希望能给想开实体店或创业的朋友们提供有价值的参考。 1、…...
一些可以访问gpt的方式
1、Coze扣子是新一代 AI 大模型智能体开发平台。整合了插件、长短期记忆、工作流、卡片等丰富能力,扣子能帮你低门槛、快速搭建个性化或具备商业价值的智能体,并发布到豆包、飞书等各个平台。https://www.coze.cn/ 2、https://poe.com/ 3、插件阿里…...
springer模板参考文献不显示
Spring期刊模板网站,我的问题是23年12月的版本 https://www.springernature.com/gp/authors/campaigns/latex-author-support/see-where-our-services-will-take-you/18782940 参考文献显示问好,在sn-article.tex文件中,这个sn-mathphys-num…...
【【C语言简单小题学习-1】】
实现九九乘法表 // 输出乘法口诀表 int main() {int i 0;int j 0;for (i 1; i < 9; i){for (j 1; j < i;j)printf("%d*%d%d ", i , j, i*j);printf("\n"); }return 0; }猜数字的游戏设计 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdi…...
mongoDB 优化(1)索引
1、创建复合索引(多字段) db.collection_test1.createIndex({deletedVersion: 1,param: 1,qrYearMonth: 1},{name: "deletedVersion_1_param_1_qrYearMonth_1",background: true} ); 2、新增索引前: 执行查询: mb.r…...
stable diffusion webUI之赛博菩萨【秋葉】——工具包新手安裝与使用教程
stable diffusion webUI之赛博菩萨【秋葉】——工具包新手安裝与使用教程 AI浪潮袭来,还是学习学习为妙赛博菩萨【秋葉】简介——(葉ye,四声,同叶)A绘世启动器.exe(sd-webui-aki-v4.6.x)工具包安…...
鸿蒙应用程序包安装和卸载流程
开发者 开发者可以通过调试命令进行应用的安装和卸载,可参考多HAP的调试流程。 图1 应用程序包安装和卸载流程(开发者) 多HAP的开发调试与发布部署流程 多HAP的开发调试与发布部署流程如下图所示。 图1 多HAP的开发调试与发布部署流程 …...
C语言数组全面解析:从初学到精通
数组 1. 前言2. 一维数组的创建和初始化3. 一维数组的使用4. 一维数组在内存中的存储5. 二维数组的创建和初始化6. 二维数组的使用7. 二维数组在内存中的存储8. 数组越界9. 数组作为函数参数10. 综合练习10.1 用函数初始化,逆置,打印整型数组10.2 交换两…...
2024-02-28(Kafka,Oozie,Flink)
1.Kafka的数据存储形式 一个主题由多个分区组成 一个分区由多个segment段组成 一个segment段由多个文件组成(log,index(稀疏索引),timeindex(根据时间做的索引)) 2.读数据的流程 …...
Window下编写的sh文件在Linux/Docker中无法使用
Window下编写的sh文件在Linux/Docker中无法使用 一、sh文件目的1.1 初始状态1.2 目的 二、过程与异常2.1 首先获取标准ubuntu20.04 - 正常2.2 启动ubuntu20.04容器 - 正常2.3 执行windows下写的preInstall文件 - 报错 三、检查和处理3.1 评估异常3.2 处理异常3.3 调整后运行测试…...
第16章-DNS
目录 1. 域名 1.1 产生背景 1.2 概述 1.3 域名的树形层次化结构 2. DNS 2.1 概述 2.2 工作机制 3. DNS查询模式 3.1 递归查询: 3.2 迭代查询: 4. 相关知识点 4.1 集中式DNS 4.2 国内通用DNS 4.3 配置DNS代理 1. 域名 1.1 产生背景 ① IP…...
Leetcoder Day27| 贪心算法part01
语言:Java/Go 理论 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。 什么时候用贪心?可以用局部最优退出全局最优,并且想不到反例到情况 贪心的一般解题步骤 将问题分解为若干个子问题找出适合的贪心策略求解每一个子…...
SpringBoot自动配置中bean的加载控制
🙈作者简介:练习时长两年半的Java up主 🙉个人主页:程序员老茶 🙊 ps:点赞👍是免费的,却可以让写博客的作者开心好久好久😎 📚系列专栏:Java全栈,…...
Linux系统运维脚本:根据菜单选择要登录到的Linux主机,方便维护多个linux服务器
目 录 一、要求 二、解决方案 (一)解决思路 (二)方案 三、脚本程序实现 (一)脚本代码和解释 1、定义hosts.txt文件 2、脚本代码 3、代码解释 (二)脚本验证 1…...
蓝桥杯练习题——二分
1.借教室 思路 1.随着订单的增加,每天可用的教室越来越少,二分查找最后一个教室没有出现负数的订单编号 2.每个订单的操作是 [s, t] 全部减去 d #include<iostream> #include<cstring> using namespace std; const int N 1e6 10; int d[…...
Java面试——Redis
优质博文:IT-BLOG-CN 一、Redis 为什么那么快 【1】完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中。 【2】数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的。 【3】采用单线…...
信号系统之复数傅立叶变换
1 实数DFT 傅里叶变换系列的所有四个成员(DFT、DTFT、傅里叶变换和傅里叶级数)都可以用实数或复数进行。由于DSP主要关心的是DFT,所以就以它为例。 可以根据以下方程定义离散傅里叶变换的实数版本: 一个 N 个样本时域信号 被分解…...
Unity - 相机画面为黑白效果
一、 在Hierarchy中创建一个Global Volume,并设置它为局部作用 二、 将场景出现的作用域范围缩小至相机所在位置,将相机包含即可。 三、添加覆盖组件Color Adjustments,并将Saturation直接拉为-100 。 此时,相机拍摄画面为黑白,场景视图中…...
哈啰Java 春招 24届
时长 1h 3. 为什么使用分布式ID,解决了什么问题 4. Leaf算法了解吗?讲一下原理和工作流程以及优缺点 5. 有没有可能导致id重复?该如何解决? 6. 项目中redis是如何运用的? 7. 项目中分布式锁是如何实现的? 8…...
华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器
第一章 引言:语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域,文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量,支撑着搜索引擎、推荐系统、…...
相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...
MySQL 8.0 事务全面讲解
以下是一个结合两次回答的 MySQL 8.0 事务全面讲解,涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容,并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念(ACID) 事务是…...
