深入解析智能指针:从实践到原理

👦个人主页晚风相伴

👀如果觉得内容对你有所帮助的话,还请一键三连(点赞关注收藏

如果内容有错或者不足的话,还望你能指出。

目录

智能指针的引入

内存泄漏

RAII

智能指针的使用及原理

std::auto_ptr

std::unique_ptr

std::shared_ptr

std::weak_ptr

定制删除器


智能指针的引入

先看一下下面的代码

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;
}

分析上面的代码,我们会发现代码运行时可能会出现以下问题

  1. 如果p1那里new失败而抛异常
  2. 如果P2那里new失败而抛异常
  3. 如果div调用那里发生除0错误而抛异常

在上面代码中如果出现以上抛异常的情况则会导致资源得不到释放从而导致内存泄漏的问题,那么有什么好的办法可以解决吗?

别说还真有一个好办法,那就是使用智能指针。

在C++中,我们经常会使用new和delete来动态分配和释放内存。但是这种手动管理的方式如果我们忘记在最后调用delete或者在delete抛异常的话就很容易出现一些内存泄漏等问题,另外,如果我们多次释放同一块内存或者在释放内存后继续使用这块内存,就会引起运行时错误。所以为了解决这里的这些问题,C++就引入了智能指针的概念。

智能指针的概念

智能指针是一种特殊的数据类型,其目的是管理动态分配的资源,尤其是堆内存。智能指针是用类模板的方式实现的并且使用RAII(资源获取即初始化)技术来确保资源的正确释放,从而避免内存泄漏和野指针的问题。

内存泄漏

 在介绍智能指针之前先来了解一下什么是内存泄漏以及内存泄漏的危害。

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

内存泄漏的危害:如果长期运行的程序出现内存泄漏,影响会很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

可以下面这段代码直观体会一下内存泄漏的危害

int main()
{char* p = new char[1024 * 1024 * 1024];cout << (void*)p << endl;return 0;
}

在代码没有运行起来时,我的内存是这么多。

在代码运行起来后,一瞬间我的内存就少了1个G。

可见如果代码中有内存泄漏的问题将会发生很严重的后果。

👍RAII

RAII(Resource Acquisition Is Initialization 资源获取即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的声明周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给一个对象。这种做法有两大好处:

  1. 不需要显示地释放资源。
  2. 采用这种方式对象所需的资源在其生命周期内始终保持有效。

采用RAII的思想来设计一个SmartPtr

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

这样上面的抛异常所带来的内存泄漏问题就能迎刃而解了

 

智能指针的使用及原理

在上面利用RAII思想设计的SmartPtr还不能称作上智能指针,因为上面的SmartPtr只是实现了构造和析构函数,还没有做到像一个指针一样支持解引用和->访问。

因此基于这样的需求我们就需要使用operator重载来实现相关功能。

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << "Delete:" << _ptr << endl;delete _ptr;}}//解引用T& operator*(){return *_ptr;}//->引用T* operator->(){return _ptr;}
private:T* _ptr;
};int main()
{SmartPtr<pair<string, int>> sp1(new pair<string, int>("print", 1));cout << sp1->first << ":" << sp1->second << endl;return 0;
}

👊std::auto_ptr

auto_ptr文档介绍

auto_ptr是C++98版本提供的智能指针,但是auto_ptr设计的并不好,导致很多人对它的意见很大以及很多公司都明确规定不去使用它。

auto_ptr的原理其实是使用了一个管理权转移的思想。先来看看库里面auto_ptr是怎么回事,然后我们就自己动手来简单实现一下auto_ptr。

class A
{
public:A(){}~A(){cout << "~A()" << endl;}int _a1 = 0;int _a2 = 0;
};void test_auto_ptr()
{std::auto_ptr<A> ap1(new A);ap1->_a1++;ap1->_a2++;std::auto_ptr<A> ap2(ap1);ap1->_a1++;ap1->_a2++;ap2->_a1++;ap2->_a2++;cout << ap2->_a1 << endl;cout << ap2->_a2 << endl;std::auto_ptr<A> ap3(new A);ap2 = ap3;ap2->_a1++;ap2->_a2++;cout << ap2->_a1 << endl;cout << ap2->_a2 << endl;
}

可以看出auto_ptr在进行拷贝时其实是用了一个管理权转移的思想,将ap1对A的管理权转移到了ap2,之后再将ap1置空,所以上面程序再往下运行就会崩溃。它的赋值操作也是采用同样的思想。

 

知道了怎么回事之后下面我们自己就动手实现一下吧

namespace hjx
{//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)//不能是自己{if (_ptr)//判断一下是否为空{cout << "Delete:" << _ptr << endl;//方便观察结果delete _ptr;}_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){cout << "Delete:" << _ptr << endl;//方便观察结果delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

🔥std::unique_ptr

 unique_ptr文档介绍

 unique_ptr对象拥有对动态分配的内存资源的唯一所有权,不能进行拷贝构造和赋值操作。

 下面就简单模拟实现一下unique_ptr来了解它的原理吧

namespace hjx
{//unique_ptr不支持拷贝和赋值template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//使用C++11中的delete将拷贝构造和赋值禁掉unique_ptr(unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;	~unique_ptr(){if (_ptr){cout << "Delete:" << _ptr << endl;delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

🔥std::shared_ptr

 shared_ptr文档介绍

shared_ptr原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享,在对象被销毁时,其引用计数减一。如果引用计数减为0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果还没减为0,就说明除了自己还有其它对象在使用该资源,不能使用该资源,否则其它对象就被成野指针了。

下面我们就简单模拟实现一下shared_ptr吧

namespace hjx
{//shared_ptr采用引用计数的方式template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pCount(sp._pCount){(*_pCount)++;//共同管理新资源,++计数}void Release(){if (--(*_pCount) == 0){cout << "Delete:" << _ptr << endl;//方便观察现象delete _ptr;delete _pCount;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//考虑自己给自己赋值的情况if (_ptr != sp._ptr){//减减被赋值对象的计数,如果是最后一个对象,要释放资源Release();//共同管理新资源,++计数_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}T* get()const{return _ptr;}private:T* _ptr;int* _pCount;//用一个指针来记录计数值};
}

但是shared_ptr也并不是完美的,它如果碰到循环引用的情况就会出现问题了

class Node
{
public:int _val;std::shared_ptr<Node> _next;std::shared_ptr<Node> _prev;~Node(){cout << "~Node()" << endl;}
};void test_shared_ptr()
{//循环引用问题std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}

从打印结果来看,它并没有完成析构的一个操作

 下面几幅图就很好的诠释了循环引用的问题

❔该如何解决这里的问题呢?那就需要用到下面即将要介绍的weak_ptr了 

🔥std::weak_ptr

 weak_ptr文档介绍

weak_ptr不是常规的智能指针,没有RAII,不支持直接管理资源,它被设计出来主要是和shared_ptr搭配使用,解决循环引用的问题。

使用weak_ptr时不会增加引用计数,所以我们就可以将上面问题中的_prev和_next换成weak_ptr就能解决问题了。

class Node
{
public:int _val;std::weak_ptr<Node> _next;std::weak_ptr<Node> _prev;~Node(){cout << "~Node()" << endl;}
};void test_shared_ptr()
{std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}

下面就来简单模拟实现一下weak_ptr吧 

namespace hjx
{//weak_ptr是个辅助型智能指针,用来解决shared_ptr循环引用问题template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}//拷贝和赋值不增加引用计数weak_ptr(const weak_ptr<T>& wp):_ptr(wp._ptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}weak_ptr<T>& operator=(const weak_ptr<T>& wp){_ptr = wp._ptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

👀定制删除器

当我们动态开辟一块数组空间时(如new int[10]),我们必须得使用delete[]来释放空间,否则程序将会崩溃。

例如下面的例子

class A
{
public:A(){}~A(){cout << "~A()" << endl;}int _a1 = 0;int _a2 = 0;
};int main()
{std::shared_ptr<A> sp2(new A[10]);//程序崩溃return 0;
}

因为shared_ptr默认使用的是delete来释放空间

其实当我们使用new来动态开辟一块数组空间时,会多开辟4个字节的空间用来存放这块空间的大小,而我们的sp2正好是指向的这4个字节的空间,当使用delete来释放空间时sp2不会跳过这4个字节的空间,所以导致类型不匹配,进而导致程序就崩溃了。

 

所以我们的shared_ptr还要设计一个仿函数删除器来解决这里的问题。

    template<class T>struct Delete{void operator()(T* ptr){delete ptr;}};//shared_ptr采用引用计数的方式template<class T, class D = Delete<T>>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pCount(sp._pCount){(*_pCount)++;//共同管理新资源,++计数}void Release(){if (--(*_pCount) == 0){/*cout << "Delete:" << _ptr << endl;//方便观察现象delete _ptr;*/D()(_ptr);delete _pCount;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//考虑自己给自己赋值的情况if (_ptr != sp._ptr){//减减被赋值对象的计数,如果是最后一个对象,要释放资源Release();//共同管理新资源,++计数_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}T* get()const{return _ptr;}private:T* _ptr;int* _pCount;//用一个指针来记录计数值};//仿函数删除器template<class T>struct DeleteArray{void operator()(T* ptr){//cout << "delete[]" << ptr << endl;//可不打印delete[] ptr;}};template<class T>struct Free{void operator()(T* ptr){//cout << "free[]" << ptr << endl;//可不打印free(ptr);}};

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

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

相关文章

STM32F10x移植FreeRTOS

一、获取FreeRTOS源码 &#xff08;1&#xff09;登录FreeRTOS官网&#xff1a;www.freertos.org&#xff0c;下载第一个压缩包 &#xff08;2&#xff09;通过GitHub网站&#xff1a;github.com/FreeRTOS/FreeRTOS下载&#xff0c;由于该网站服务器在国外&#xff0c;所以访问…

1688快速获取整店铺列表 采集接口php Python

在电子商务的浪潮中&#xff0c;1688平台作为中国领先的批发交易平台&#xff0c;为广大商家提供了一个展示和销售商品的广阔舞台&#xff1b;然而&#xff0c;要在众多店铺中脱颖而出&#xff0c;快速获取商品列表并进行有效营销是关键。 竞争对手分析 价格比较&#xff1a;…

【强训笔记】day12

NO.1 思路&#xff1a;哈希表&#xff0c;建立bool数组&#xff0c;将要删除的字符串存入哈希表&#xff0c;并标为true&#xff0c;再遍历要做处理的字符串&#xff0c;如果在哈希表中为false&#xff0c;就输出。 代码实现&#xff1a; #include <iostream> #includ…

喜报 | 擎创科技荣获NIISA联盟2023年度创新技术特等奖!

为深入实施创新驱动发展战略&#xff0c;紧紧把握全球科技革命和产业变革方向&#xff0c;密切跟踪前沿科技新趋势&#xff0c;经科技部中国民营促进会业务主管部门批准以及国家互联网数据中心产业技术创新战略联盟&#xff08;以下简称联盟&#xff09;总体工作安排&#xff0…

代码随想录算法训练营第十八天:二叉树的层序遍历(中间放假)

代码随想录算法训练营第十八天&#xff1a;二叉树的层序遍历&#xff08;中间放假&#xff09; ‍ ​​ 102.二叉树的层序遍历 力扣题目链接(opens new window) 给你一个二叉树&#xff0c;请你返回其按 层序遍历 得到的节点值。 &#xff08;即逐层地&#xff0c;从左到右…

Java 框架安全:Struts2 漏洞序列测试.

什么是 Struts2 框架 Struts 2 是一个用于创建企业级 Java 应用程序的开源框架。它是一个 MVC&#xff08;模型-视图-控制器&#xff09;框架&#xff0c;用于开发基于 Java EE&#xff08;Java Platform, Enterprise Edition&#xff09;的 Web 应用程序。Struts 2 主要解决…

【Linux】HTTPS

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 &#x1f3e0;个人专栏&#xff1a;Linux 目录 &#x1f449;&#x1f3fb;HTTPS协议概念&#x1f449;&#x1f3fb;加密为什么要进行加密 &#x1f449;&#x1f3fb;常见的加密方式对称加密…

【MATLAB源码-第204期】基于matlab的语音降噪算法对比仿真,谱减法、维纳滤波法、自适应滤波法;参数可调。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 语音降噪技术的目的是改善语音信号的质量&#xff0c;通过减少或消除背景噪声&#xff0c;使得语音更清晰&#xff0c;便于听者理解或进一步的语音处理任务&#xff0c;如语音识别和语音通讯。在许多实际应用中&#xff0c;如…

基于FPGA的DDS波形发生器VHDL代码Quartus仿真

名称&#xff1a;基于FPGA的DDS波形发生器VHDL代码Quartus仿真&#xff08;文末获取&#xff09; 软件&#xff1a;Quartus 语言&#xff1a;VHDL 代码功能&#xff1a; DDS波形发生器VHDL 1、可以输出正弦波、方波、三角波 2、可以控制输出波形的频率 DDS波形发生器原理…

怎样扫描二维码后看图片?图片二维码的制作方式

二维码是一种可以用来存储大量内容&#xff0c;通过扫描二维码的方式来向其他人提供内容&#xff0c;比较常见的展示内容有视频、图片、文件、文本、音频等。那么图片生成二维码的方法是什么样的呢&#xff1f;通过扫码查看图片&#xff0c;可以不下载的图片的同时快速预览内容…

护眼台灯品牌排行前十名有哪些?最新护眼台灯品牌排行前十名分享

在近几年&#xff0c;一个引人关注的健康现象是青少年近视问题的日益加剧。统计数字显示&#xff0c;近视的发病率不断攀升&#xff0c;令人忧心地发现&#xff0c;许多才刚步入小学一年级的孩子们&#xff0c;便已佩戴起了厚重的眼镜。其中最主要的原因就在于学习过程对数码设…

当AI遇见现实:数智化时代的人类社会新图景

文章目录 一、数智化时代的机遇二、数智化时代的挑战三、如何适应数智化时代《图解数据智能》内容简介作者简介精彩书评目录精彩书摘强化学习什么是强化学习强化学习与监督学习的区别强化学习与无监督学习的区别 前言/序言 随着科技的日新月异&#xff0c;我们步入了一个前所未…

电镀回用水除镍树脂的一种设备方法

【除镍树脂CH-90】是一种专门设计用于去除溶液中镍离子的高效吸附材料。随着工业发展和环境保护要求的提高&#xff0c;除镍树脂CH-90在废水处理、金属回收和环境保护等领域的应用越来越广泛。 除镍树脂CH-90的特点在于其高效的吸附性能和选择性。该树脂能够快速、准确地去除溶…

Deeplab的复现(pytorch实现)

DeepLab复现的pytorch实现 本文复现的主要是deeplabv3。使用的数据集和之前发的文章FCN一样&#xff0c;没有了解的可以移步到之前发的文章中去查看一下。 1.该模型的主要结构 对于代码部分&#xff0c;主要只写了模型部分的&#xff0c;其他部分内容基本和FCN的一致&#xf…

Anaconda删除虚拟环境目录pkgs和envs|conda瘦身

这个文件夹里面是专门放不同环境中的包的&#xff0c;只是没有区分环境&#xff0c;都混在一起了&#xff0c; 一般在想要删除一个虚拟环境&#xff0c;除了在命令行中输入conda remove -n your_env_name(虚拟环境名称) --all 然后在envs中删除虚拟环境的文件夹&#xff0c; 还…

密码学《图解密码技术》 记录学习 第十三章

目录 第十三章 13.1 本章学习的内容 13.2 PGP 简介 13.2.1 什么是 PGP 13.2.2 关于 OpenPGP 13.2.3关于GNU Privacy Guard 13.2.4 PGP 的功能 公钥密码 数字签名 单向散列函数 证书 压缩 文本数据 大文件的拆分和拼合 13.3 生成密钥对 13.4 加密与解密 13.4.1 加密 生成…

《ESP8266通信指南》12-Lua 固件烧录

往期 《ESP8266通信指南》11-Lua开发环境配置-CSDN博客 《ESP8266通信指南》10-MQTT通信&#xff08;Arduino开发&#xff09;-CSDN博客 《ESP8266通信指南》9-TCP通信&#xff08;Arudino开发&#xff09;-CSDN博客 《ESP8266通信指南》8-连接WIFI&#xff08;Arduino开发…

牛客周赛 Round 41 C-F

C 小红的循环移位 思路&#xff1a; 一个数是不是四的倍数&#xff0c;只用看最后两位是否能够整除4即可。 #include <bits/stdc.h>using namespace std; const int N 1e6 5; typedef long long ll; typedef pair<ll, ll> pll; typedef array<ll, 3> p3;…

分布式与一致性协议之ZAB协议(五)

ZAB协议 ZAB集群如何从故障中恢复 如果我们想把ZAB集群恢复到正常状态&#xff0c;那么新领导者就必须确立自己的领导关系&#xff0c;成为唯一有效的领导者&#xff0c;然后作为主节点"领导"各备份节点一起处理读写请求 如何确立领导关系 前面提到&#xff0c;选…

VISO流程图之子流程的使用

子流程的作用 整个流程图的框图多而且大&#xff0c;进行分块&#xff1b;让流程图简洁对于重复使用的流程&#xff0c;可以归结为一个子流程图&#xff0c;方便使用&#xff0c;避免大量的重复性工作&#xff1b; 新建子流程 方法1&#xff1a; 随便布局 框选3 和4 &#…