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

👦个人主页晚风相伴

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

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

目录

智能指针的引入

内存泄漏

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://xiahunao.cn/news/3022273.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]秦时明月6.2魔改版_搭建架设教程_附GM工具_安卓苹果

本教程仅限学习使用&#xff0c;禁止商用&#xff0c;一切后果与本人无关&#xff0c;此声明具有法律效应&#xff01;&#xff01;&#xff01;&#xff01; 教程是本人亲自搭建成功的&#xff0c;绝对是完整可运行的&#xff0c;踩过的坑都给你们填上了 一. 演示视频 秦时明…

射频无源器件之耦合器

一. 耦合器的作用 在射频电路中,射频耦合器将一路微波功率按比例分成几路,用于检测或监测信号,如功率测量和波检测,还可改变信号的幅度、相位等特性,以满足不同的通信需求。根据输入与耦合端的功率差,常被分为5dB、6dB、10dB等耦合器。射频耦合器的类型主要包括定向耦合…

为软件教学文档增加实践能力

为了更方便软件教学&#xff0c;我们在凌鲨(OpenLinkSaas)上增加了公共资源引用的功能。 目前可以被引用的公共资源: 微应用常用软件公共知识库Docker模板 引用公共资源 引用微应用 目前微应用包含了主流数据库&#xff0c;终端等工具&#xff0c;可以方便的进行各种相关实…

OpenCV|简单绘制一个矩形

OpenCV中的rectangle() 为绘制矩形命令&#xff0c;形式如下&#xff1a; # (img: cv2.typing.MatLike, pt1: cv2.typing.Point, pt2: cv2.typing.Point, color: cv2.typing.Scalar, thickness: int ..., lineType: int ..., shift: int ...)cv2.rectangle(img, pt1, pt2, …

2024软件测试自动化面试题(含答案)

1.如何把自动化测试在公司中实施并推广起来的&#xff1f; 选择长期的有稳定模块的项目 项目组调研选择自动化工具并开会演示demo案例&#xff0c;我们主要是演示selenium和robot framework两种。 搭建自动化测试框架&#xff0c;在项目中逐步开展自动化。 把该项目的自动化…

数据结构-线性表-链表-2.3-6

有一个带头结点的单链表L&#xff0c;设计一个算法使其元素递增有序。 void sort(Linklist &L){LNode *pL->next,*pre;LNode *rp->next;p->nextNULL;pr;while(p){rp->next;preL;while(pre->next!NULL&&pre->next->data<p->data){prepre…

县供电公司员工向媒体投稿发文章用亲身经历告诉你并不难

在县供电公司的日子里,我肩负着一项至关重要的使命——信息宣传工作。这不仅仅是一份职责,更是连接公司与外界的桥梁,通过新闻稿件传递我们的声音,展示我们的成果。然而,回忆起刚刚踏入这个领域的时光,那段经历至今让我感慨万千。 初涉投稿,步履维艰 刚接手这项工作时,我的投稿…

暴打前任互动玩法开播教程

暴打前任互动玩法开播教程 暴打前任互动玩法开播教程【相关直播互动插件咩播需要额外官方购买】 网盘自动获取 链接&#xff1a;https://pan.baidu.com/s/1lpzKPim76qettahxvxtjaQ?pwd0b8x 提取码&#xff1a;0b8x

安装docker20.10.18版本步骤

安装docker20.10.18版本步骤 准备低版本安装包 #安装20.10.18版本的dockercd /opt #切换目录#上传需要的docker20.10.18.zip安装包unzip docker20.10.18.zip #解压cd docker20.10.18/ #切换目录yum install -y *.rpm #安装systemctl enable --now docker.service #开机自启并…

NineData亮相2024中国移动算力网络大会

4月28日至29日&#xff0c;2024中国移动算力网络大会在苏州召开。大会以“算力网络点亮AI新时代”为主题&#xff0c;全面展示了中国移动最新算力网络成果与能力。江苏省委常委、苏州市委书记刘小涛&#xff0c;副省长赵岩出席开幕式并致辞。内蒙古自治区副主席白清元出席。中国…

redis集群-主从机连接过程

首先从机需要发送自身携带的replid和offset向主机请求连接 replid&#xff1a;replid是所有主机在启动时会生成的一个固定标识&#xff0c;它表示当前复制流的id&#xff0c;当从机第一次请求连接时&#xff0c;主机会将自己的replid发送给从机&#xff0c;从机在接下来的请求…

5月8日学习记录

_[FBCTF2019]RCEService&#xff08;preg_match函数的绕过&#xff09; 涉及知识点&#xff1a;preg_match函数绕过&#xff0c;json的格式&#xff0c;正则回溯 打开环境&#xff0c;要求用json的格式输入 搜索学习一下json的语法规则 数组&#xff08;Array&#xff09;用方括…

无意的一次学习,竟让我摆脱了Android控制?

由于鸿蒙的爆火&#xff0c;为了赶上时代先锋。到目前为止也研究过很长一段时间。作为一名Android的研发人员&#xff0c;免不了对其评头论足&#xff0c;指导文档如何写才算专业&#xff1f;页面如何绘制&#xff1f;页面如何跳转&#xff1f;有没有四大组件等等。 而Harmony…

PDF转word转ppt软件

下载地址&#xff1a;PDF转word转ppt软件.zip 平时工作生活经常要用到PDF转word转ppt软件&#xff0c;电脑自带的又要开会员啥的很麻烦&#xff0c;现在分享这款软件直接激活就可以免费使用了&#xff0c;超级好用&#xff0c;喜欢的可以下载

党建教育vr虚拟现实展厅真正实现了绿色、低碳的展示方式

在数字化浪潮席卷的今天&#xff0c;传统企业门户官网已难以满足企业日益增长的展示需求。面对这一挑战&#xff0c;北京华锐凭借深厚的行业经验和领先的技术实力&#xff0c;为您提供全新的元宇宙虚拟展厅制作服务&#xff0c;助您轻松打破现实与虚拟的界限&#xff0c;开启企…

第八届大数据与物联网国际会议(BDIOT 2024)即将召开!

第八届大数据与物联网国际会议(BDIOT 2024)将于2024年9月14-16日在澳门圣若瑟大学举行。数聚未来&#xff0c;物联世界&#xff01;BDIOT 2024旨在搭建为各位与会代表展示自己研究成果、分享经验、建立联系和开展合作的平台&#xff0c;共同探讨大数据与物联网领域的未来发展方…

人工智能-2024期中考试

前言 人工智能期中考试&#xff0c;认真准备了但是没考好&#xff0c;结果中游偏下水平。 第4题没拿分 &#xff08;遗传算法&#xff1a;知识点在课堂上一笔带过没有细讲&#xff0c;轮盘赌算法在书本上没有提到&#xff0c;考试的时候也没讲清楚&#xff0c;只能靠猜&…

鸿蒙OpenHarmony开发板:【子系统配置规则】

子系统 子系统配置规则 通过build仓下的subsystem_config.json可以查看所有子系统的配置规则。 {"arkui": {"path": "foundation/arkui", # 路径"name": "arkui" # 子系统名},"ai": {&q…

Java内存是怎样分配的

Java内存是怎样分配的 一、 1. 有些编程语言编写的程序会直接向操作系统请求内存&#xff0c;而 Java 语言为保证其平台无关性&#xff0c;并不允许程序直接向操作系统发出请求&#xff0c;而是在准备执行程序时由Java虚拟机&#xff08;JVM&#xff09;向操作系统请求一定的…

微信小程序开发-数据事件绑定

&#x1f433;简介 数据绑定 数据绑定是一种将小程序中的数据与页面元素关联起来的技术&#xff0c;使得当数据变化时&#xff0c;页面元素能够自动更新。这通常使用特定的语法&#xff08;如双大括号 {{ }}&#xff09;来实现&#xff0c;以便在页面上展示动态数据。 事件绑…