智能指针(C++11)

智能指针的使用

问题

我们在平时写程序的时候,有些情况下不可避免地会遇见内存泄露的情况。内存泄露是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。例如下面这个例子,内存泄漏不易被察觉。

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

程序首先会调用func函数,当func函数中执行到“ cout << div() << endl ”这句代码时,就会跳转到div函数中依次执行,当我们把b设置为0时,这时就会抛异常。程序的执行流就直接跳转到主函数的catch块中执行,最终导致func函数中申请到的资源没有被释放,造成内存泄露。

解决方案一

我们可以利用异常的重新捕获来解决,在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。

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

解决方案二

上述问题也可以使用智能指针进行解决

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete: " << _ptr << endl;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()
{SmartPtr<int> sp(new int[10]);//...cout << div() << endl;//...
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
  • 此外,为了让SmartPtr对象能够像原生指针一样使用,还需要对*->运算符进行重载。

这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。

智能指针的原理

实现智能指针时需要考虑以下三个方面的问题:

  1. 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
  2. *->运算符进行重载,使得该对象具有像指针一样的行为。
  3. 智能指针对象的拷贝问题。

 RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术。 简而言之就是把资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源

但是我们对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。

int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(sp1); //拷贝构造SmartPtr<int> sp3(new int);SmartPtr<int> sp4(new int);sp3 = sp4; //拷贝赋值return 0;
}

这是由于编译器默认生成的拷贝构造函数和赋值重载函数对内置类型完成的是浅拷贝,若将一个SmartPtr对象原封不动地拷贝给另外一个SmartPtr对象,这时两个SmartPtr对象就指向同一块内存空间。当SmartPtr对象销毁时就会调用析构函数,这样就导致了同一块空间被析构了两次,程序因而会崩溃。

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

C++中的智能指针

auto_ptr

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。比如:

int main()
{// C++98 一般实践中,很多公司明确规定不要用这个auto_ptr<A> ap1(new A(1));auto_ptr<A> ap2(new A(2));auto_ptr<A> ap3(ap1);// 崩溃//ap1->_a++;ap3->_a++;return 0;
}

发生管理权转移后,再进行拷贝时就会把被拷贝对象的资源管理权转移给拷贝对象导致被拷贝对象悬空,这也就造成了很大的隐患,当访问被拷贝对象时程序就会造成程序崩溃。

auto_ptr的模拟实现

auto_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使auto_ptr对象具有像原生指针一样的行为。
  • 在拷贝构造函数中,通过传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//管理权转移auto_ptr(auto_ptr<T>& a):_ptr(a._ptr){a._ptr = nullptr;}private:T* _ptr;};

unique_ptr

unique_ptr是C++11中引入的智能指针,unique_ptr通过禁止拷贝的方式解决智能指针的拷贝问题,它是个简单粗暴的办法,这样做也能保证资源不会被多次释放。但是禁止拷贝的办法也不是一个万全的办法,因为有些场景就是要用到拷贝。

int main()
{unique_ptr<int> up1(new int(0));//std::unique_ptr<int> up2(up1);  //会报错return 0;
}

unique_ptr的模拟实现

unique_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使unique_ptr对象具有像原生指针一样的行为。
  • 用C++11的方式在拷贝构造和赋值重载函数后面加上=delete。
template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(unique_ptr<T>& ap) = delete;unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;private:T* _ptr;};

shared_ptr

shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。

  • 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
  • 当新增一个对象管理这块资源时,则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
  • 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。

class A
{
public:A(int a = 0):_a(a){cout << "A(int a = 0)" << endl;}~A(){cout << this;cout << " ~A()" << endl;}//private:int _a;
};

注:A类的具体内部结构如上述代码所示。

int main()
{a::shared_ptr<A> sp1(new A(1));a::shared_ptr<A> sp2(new A(2));a::shared_ptr<A> sp3(sp1);sp1->_a++;sp3->_a++;a::shared_ptr<A> sp4(sp2);a::shared_ptr<A> sp5(sp4);return 0;
}

其关系图如下图所示,其中红色方框代表的是引用计数

shared_ptr的模拟实现

shared_ptr的实现步骤如下:

  • 首先要在shared_ptr类中增加一个成员变量_pcount,代表引用计数。
  • 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理资源。
  • 在拷贝构造函数中,我们让传入的对象与之前管理资源的对象共同管理对应的资源,同时将该资源的引用计数加一。
  • 在赋值重载函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。

举例:例如将sp5赋值给sp1,那么sp1原来指向的那块资源的引用计数就要减一,否则的话就会内存泄漏,因为无论如何引用计数都减不到为0,那么这块空间也就不会释放。并且原先sp1指向的这块资源如果只有sp1在管理的话,那么就直接将其释放即可。还有一个细节问题,例如遇到“ sp1=sp1 ”自己给自己赋值这种情况的话,我们就直接返回*this即可。

  • 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放。
  • 对*和->运算符进行重载,使shared_ptr对象具有像原生指针一样的行为。
template<class T>
class shared_ptr
{
public:// RAII// 像指针一样shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}// sp6 = sp6shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr == sp._ptr)return *this;if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);return *this;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;
};

这里有个问题,为什么引用计数要放在堆区呢? 

如果将引用计数设置为int类型时,那么每个对象都有自己独立的引用计数,而我们希望的是不同的对象管理同一份资源 。其次,shared_ptr中的引用计数count也不能定义成静态成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。

weak_ptr

虽然shared_ptr非常好用,看似完美无缺,但是它在某个场景中却有致命的缺陷。

我们先定义一个节点类,并且将_next和_prev成员变量的类型改为shared_ptr类型。

struct Node
{A _val;a::shared_ptr<Node> _next;a::shared_ptr<Node> _prev;~Node(){cout<<"~Node"<<endl;}
};

 接着我们再将sp1和sp2相互链接,然后运行程序观察结果。

int main()
{// 循环引用shared_ptr<Node> sp1(new Node);shared_ptr<Node> sp2(new Node);cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;sp1->_next = sp2;sp2->_prev = sp1;cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;return 0;
}

我们发现这时程序运行结束后两个结点都没有被释放,但如果去掉链接节点时的两句代码中的任意一句,那么这两个节点就都能够正确释放,根本原因就是因为这两句链接节点的代码导致了循环引用

出现循环引用的原因

当以new的方式申请到两个Node节点并交给两个智能指针管理后,这两个资源对应的引用计数都为1。接着我们将这两个节点链接起来后,资源1当中的next成员与sp2一同管理资源2,资源2中的prev成员与sp1一起管理资源1,此时这两个资源对应的引用计数都被加到了2。

当出了main函数的作用域后,sp1和sp2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1

根据上图可知_prev管着左边的节点,_next管着右边的节点。当右边的节点析构时,_prev才会析构,而右边的节点是当_next析构它才会析构,而_next析构需要左边的节点析构才能析构。这时就形成了一个死循环,最终导致资源无法被释放。

而如果链接节点时只进行一个链接操作,那么当sp1和sp2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,当这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都会被释放了。这就是为什么只进行一个连接操作时这两个节点就都能够正确释放的原因。

针对上述问题,C++11引入了weak_ptr。其中我们最需要注意的一点是weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。

weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数

我们将_next和_prev的类型改为weak_ptr 

struct Node
{A _val;weak_ptr<Node> _next;weak_ptr<Node> _prev;~Node(){cout<<"~Node"<<endl;}
};

接着再执行上述代码就会发现两个节点能正确释放,并且根据use_count链接前后两次打印的内容发现weak_ptr不会增加对应资源的引用计数

weak_ptr的模拟实现

weak_ptr的实现步骤如下:

  • 提供一个无参的构造函数,比如new Node这句代码就会调用weak_ptr的无参的构造函数。
  • 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr管理的资源。
  • 支持用shared_ptr对象赋值给weak_ptr对象,赋值时获取shared_ptr管理的资源。
  • 对*和->运算符进行重载,使weak_ptr对象具有像原生指针一样的行为。
template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

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

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

相关文章

Vue tree自定义滚动条位置

贴一张效果图&#xff0c;我的效果不方便贴出来 实现支持&#xff1a; 1、懒加载 2、普通加载 下面贴关键思想&#xff1a; document有一个获取element元素的方法。 let element document.getElementById(tree); let arr document.querySelectorAll(".nodelModel&quo…

【JDK常用的API】包装类

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

【IDEA】安装教程

目录 一、安装IDEA 二、激活IDEA 总结 一、安装IDEA 点击idea安装包->点击next->选择安装的路径->勾选创建桌面的快捷方式->勾选将bin目录添加到环境变量->勾选文件夹当做项目工程 打开下面这四个不勾选&#xff0c;勾选表示打开文件将以下面的格式打开 安装…

Python绘制线图之plt.plot()的介绍以及使用

在Python中plt.plot是matplotlib库中的一个函数,用于绘制点和线,并对其样式进行控制,下面这篇文章主要给大家介绍了关于Python绘制线图之plt.plot()的介绍以及使用的相关资料,需要的朋友可以参考下 plt.plot() 是Matplotlib库中用于绘制线图&#xff08;折线图&#xff09;的主…

YOLOv9改进策略 :主干篇 | 南开大学提出LSKNet,遥感旋转目标检测新SOTA ,ICCV 2023

💡💡💡本文改进内容: 动态调整特征提取骨干的感受野,以便更有效地处理被检测大小物体的不同的检测能力,也就是说可以有效提升检测数据集当中存在大小目标的检测能力 改进结构图如下: 《YOLOv9魔术师专栏》将从以下各个方向进行创新: 【原创自研模块】【多组合点优…

OpenEuler华为欧拉系统安装教程及联网配置

OpenEuler简介 openEuler是一款开源操作系统。当前openEuler内核源于Linux&#xff0c;支持鲲鹏及其它多种处理器&#xff0c;能够充分释放计算芯片的潜能&#xff0c;是由全球开源贡献者构建的高效、稳定、安全的开源操作系统&#xff0c;适用于数据库、大数据、云计算、人工智…

java入门学习Day01

本篇文章主要是学会如何使用IDEA&#xff0c;和运行第一个java文件。 java环境安装&#xff1a;Windows下Java环境配置教程_windows java环境配置-CSDN博客 IDEA安装&#xff1a;IDEA 2023.2.5 最新激活码,注册码&#xff08;亲测好用&#xff09; - 异常教程 以上两个链接…

安装VS2022社区版

Visual Studio 2022 平台的使用 1.Visual Studio 的下载地址&#xff1a; https://visualstudio.microsoft.com/zh-hans/downloads/ 2.安装步骤简要记录 耐心等待安装完成 参考链接&#xff1a;Visual Studio 2022安装教程(非常详细)&#xff0c;从零基础入门到精通&…

Thinkphp - 详细实现网站系统登录功能,附带 Mysql 数据库设置、Web 前端展示界面、信息校验等(详细代码,即设计过程)

前言 登录功能&#xff0c;是我们几乎开发每个系统都必须的模块。 登录功能设计思路&#xff0c;主要包括几个方面。 用户输入网址展示登录页面用户输入用户名&#xff0c;密码等点击登录进行信息校验校验通过之后&#xff0c;记录用户登录信息&#xff0c;跳转指定页面用户校…

EI期刊和EI会议有哪些不同?别再傻傻分不清

EI工程索引是综合性检索机构&#xff0c;是三个著名学术检索系统之一&#xff0c;EI工程索引也分为EI期刊和EI会议&#xff0c;那么两者有哪些不同&#xff1f;作者又该如何选&#xff1f;本文系统分享一下相关的知识&#xff0c;仅供学术人员参考&#xff1a; 第一、文章质量不…

RCG自条件是如何添加到 Pixel Generator上的?

在自条件的训练过程中&#xff0c;需要将图像经过Pretrained encoder的表征Rep输入进已有的Pixel Generator上&#xff0c;目前RCG是向四种Pixel Generator上加入了自条件&#xff0c;关于它是如何将rep加到Pixel Generator上的&#xff0c;我来总结一下&#xff1a; 一、Pixel…

【前端Vue】Vue从0基础完整教程第4篇:面经PC端 - Element (下)【附代码文档】

Vue从0基础到大神学习完整教程完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;vue基本概念&#xff0c;vue-cli的使用&#xff0c;vue的插值表达式&#xff0c;{{ gaga }}&#xff0c;{{ if (obj.age > 18 ) { } }}&#xff0c;vue指令&#xff0c;综合…

树的重心——树的结构

树的重心是指对于某个点&#xff0c;将其删除后&#xff0c;可以使得剩余联通块的最大值最小。也就等价于一某个点为根的树&#xff0c;将根删除后&#xff0c;剩余的若干棵子树的大小最小。 例如下图的树的重心就是2。 性质&#xff1a; 性质一&#xff1a;重心的若干棵子树打…

Vue使用el-statistic和el-card显示大屏中的统计数据

​ 一、页面内容&#xff1a; <el-row :gutter"20"><el-col :span"6"><el-card class"box-card"><div><el-statisticgroup-separator",":precision"2":value"value2":title"tit…

【娱乐】战双帕弥什游戏笔记攻略

文章目录 Part.I IntroductionChap.I Information Part.II 新手攻略Chap.I 角色和武器挑选Chap.II 新手意识推荐 Part.II 阵容搭配Chap.I 一拖二Chap.II 毕业队 Reference Part.I Introduction 2019年12月5日全平台公测。 偶然间入坑战双&#xff0c;玩了几天&#xff0c;觉得…

elasticsearch基础应用

1._cat接口 | _cat接口 | 说明 | | GET /_cat/nodes | 查看所有节点 | | GET /_cat/health | 查看ES健康状况 | | GET /_cat/master | 查看主节点 | | GET /_cat/indices | 查看所有索引信息 | es 中会默认提供上面的几个索引&#xff0c;表头…

Hotspot虚拟机对象问题(对象头...对象创建)

目录 对象头 实例数据 对齐填充 对象是如何创建 对象头 在Hotspot虚拟机中&#xff0c;Java对象在内存中的布局大致可以分为三部分:对象头、实例数据和填充对齐。因为synchronized用的锁是存在对象头里的&#xff0c;这里我们需要重点了解对象头。如果对象头是数组类型则对…

springboot汉服推广网站

摘 要 本论文主要论述了如何使用JAVA语言开发一个汉服推广网站 &#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述汉服推广网站的当前背景以及系统开发的目的&am…

创建第一个Electron程序

前置准备 创建一个文件夹&#xff0c;如: electest进入文件夹&#xff0c;初始化npm npm init -y 安装electron依赖包 注&#xff0c;这里使用npm i -D electron会特别卡&#xff0c;哪怕换成淘宝源也不行。可以使用下面方式安装。 首先&#xff0c;安装yarn npm i -g yarn 随…

从打开电视的过程认识接口(Java)

目录 问题引入 接口 什么是接口 接口的特点 简单理解接口 接口的实现 TV接口及其实现 Controlor接口及其实现 注意 情景实现 代码 输出 问题引入 在面向对象的世界里&#xff0c;不妨设想&#xff0c;我们打开电视需要些什么呢&#xff1f;显然的&#xff0c;首…