C++:智能指针[重点!]

目录

一、关于智能指针

1、引入智能指针

2、RAII

二、详述智能指针

auto_ptr

unique_ptr

shared_tr

循环引用

weak_ptr

定制删除器

三、关于内存泄漏


一、关于智能指针

1、引入智能指针

首先引入一个例子:

在Test函数中,new了两个对象p1p2,正常来说,new的对象对应delete就可以,但是有了异常处理的情况,如果出现除0错误,则会从直接被main函数中的catch所捕获,跳过了Test函数中的delete,从而造成了内存泄漏的问题

那么为了解决上面的问题,C++就引入了智能指针

2、RAII

RAII是一种利用对象声明周期来控制程序资源的技术

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源

有两个好处:

①不需要显式地释放资源

②对象所需的资源在其生命期内始终保持有效

下面是用RAII思想delete资源,设计出来的Ptr类

只需在原有例子中改为:

就可以完美解决上述的问题

p1p2构造了r1r2,如果出现除0错误,直接被main函数的catch语句捕获,r1r2声明周期结束,会自动调用析构函数delete:

所以无论正常结束还是抛异常结束,r1r2都会调用析构函数释放资源


又因为正常new的对象,可以解引用或使用->,所以我们所写的Ptr类还需要运算符重载*和->:

这样就可以像指针一样去使用


二、详述智能指针

智能指针特点:

具有RAII特性

重载operator*和opertaor->,具有像指针一样的行为

由于C++更新迭代速度太慢了,C++11的上一版本就是C++98,中间相隔了13年之久,所以就有C++委员会的大佬组建了boost社区,充当探路者的角色,一些新语法会先在boost社区中应用,如果效果好就会被C++吸收引用

boost首先给出了scoped_ptr、shared_ptr和weak_ptr,C++11将这三种智能指针都引入了,只不过将scoped_ptr改名为了unique_ptr

auto_ptr

C++98定义了auto_ptr

auto_ptr在头文件memory

下面验证一下auto_ptr会自动调用析构函数从而delete资源

可以看出,new了一个Test对象,会自动调用析构函数


而智能指针比较难处理的地方在于:会有浅拷贝的问题:

比如p1指向一段空间,而p2拷贝p1,没有写拷贝构造编译器默认生成的是浅拷贝,会导致p2也指向这段空间,最后析构时会释放两次资源,导致出错

而怎么解决这个问题呢,首先排除深拷贝的方法,因为我们本身就是要使用浅拷贝,深拷贝违背了功能需求

而auto_ptr的解决方案是:将p2与p1的资源做交换,下面调试观察:

先看没有拷贝前,p1的地址:

拷贝后p2的地址:

很明显p2的地址变成了刚刚p1的地址,而p1被置空,如果使用者不清楚其中的规则,这样做可能会导致使用者再次使用p1中的指针时发生空指针问题,被拷贝对象出现了悬空问题

所以这里的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;}//赋值 ap1 = ap2auto_ptr<T>& operator=(auto_ptr<T>& ap){//不是自己赋值自己if (this != &ap){//自己_ptr不为空if (_ptr){delete _ptr;}//ap2的_ptr给ap1,ap2置空_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

unique_ptr

unique_ptr是C++11提出的

由于auto_ptr的拷贝有问题,所以unique_ptr不允许拷贝,也不允许赋值,如上图所示

为了不允许拷贝,也不允许赋值,C++98和C++11在底层都有各自的解决方案

C++98:底层只声明不实现,并且设为私有(为了防止类外实现)

C++11:直接在拷贝和赋值函数后面加上 = delete,使用了C++11delete新增的用法,指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

所以unique_ptr也只适用于不进行拷贝的场景,也不常用

下面是简易的实现一个unique_ptr,其他与auto_ptr类似,就是在赋值和拷贝构造那里加了delete:

template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//c++98实现方式//拷贝、赋值只声明不实现,且设为私有
//private://unique_ptr(unique_ptr<T>& ap);//unique_ptr<T>& operator=(unique_ptr<T>& ap);//c++11实现方式//拷贝、赋值都加delete,防止拷贝unique_ptr(unique_ptr<T>& ap) = delete;unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;~unique_ptr(){if (_ptr){delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

shared_tr

shared_ptr可以进行拷贝,也是C++11提出的

如上图所示,支持拷贝,也支持像指针一样使用

要想实现这种方式,底层使用了引用计数,记录当前几个对象指向这个空间,对象释放时--计数,只有最后一个析构的对象再释放资源

这种引用计数首先排除的实现方式就是增加一个私有成员int _count,这样做无法满足要求,原因是每一个对象都有一个_count,无法实现共享

还有一个方式是创建一个静态成员static int _count(静态成员需要类内声明类外初始化),这样的实现方式,如果是同一种类型的对象可以满足要求,但如果不同类型,却依然是共享一个_count,就会有问题,例如:

上述情况,我们的p1p2p3是一种类型,p4是另一种类型,满足要求的情况是p1p2p3共享一个_count计数,p4有另一个_count计数 ,因为p1p2p3与p4类型不同,但是上述实现方式却会导致p1p2p3p4只有一个_count计数,所以会出现问题

所以正确的实现方式是:在成员中增加一个int* _pcount,每次有新类型对象会调用构造函数,在构造函数中new一个新的引用计数,完美解决问题

下面是简易实现的shared_ptr的代码:

template<class T>
class shared_ptr
{
public://如果有新资源,构造时会创建一个新的_pcount,赋值为1shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}//拷贝shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}//赋值 sp2 = sp1shared_ptr<T>& operator=(const shared_ptr<T>& sp){//判断不是自己赋值自己,不能用this != &sp//因为如果前面sp2 = sp1,这时sp1和sp2是一样的//再赋值sp2 = sp1,就不能用this != &sp判断出来了if (_ptr != sp._ptr){//被赋值的对象的计数--,为0就提前释放//表示是最后一个对象,需要释放资源if (--(*_pcount) == 0){delete _ptr;delete _pcount;}//共同管理新资源_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}//返回计数个数int use_count(){return *_pcount;}//返回指针_ptr,防止weak_ptr构造时私有无法获取T* get() const{return _ptr;}~shared_ptr(){//计数为0,则delete _ptr、_pcountif (--(*_pcount) == 0){delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;//引用计数int* _pcount;
};

循环引用

shared_ptr已经可以解决大部分问题了,但是还是会有情况无法解决,即下面所说的循环引用:

上述情况中,有两个结点Node,分别是n1、n2

n1、n2中都有_next和_prev,而n1、n2在构造时也都会一个计数初始值都为1

而下面的n1->_next = n2,其实是智能指针的赋值,因为n1->_next和n2都是智能指针,所以n1->_next指向n2时n2的计数会+1,变为2

接下来的n2->_prev = n1同理,n1的计数+1也变为2

下面的运行结果可以看到,计数的情况:

可以发现,在main函数结束前,n1n2的计数都为2 

main函数结束后,n2先析构,n1后析构,n1n2计数都--,变为1

所以没有执行析构函数(没有打印~Node)

此时变为了下图这样子的情况:

左边结点的_next管着右边的结点内存块,右边结点的_prev管着左边的结点内存快

此时_next释放右边就释放(delete),_prev释放左边就释放()delete

以左边结点的_next为例,_next作为左边结点的成员,只有左边结点被delete时,调用析构函数,_next才会析构,从而把右边结点的计数减为0,释放右边结点

而左边结点什么时候析构,则是由右边结点的_prev决定的,而_next作为右边结点的成员,只有右边结点被delete时,调用析构函数,_prev才会析构

形成了循环引用的问题

总结一下循环引用问题:

即就是右边结点什么时候delete,取决于左边的_next什么时候析构,而_next什么时候析构取决于左边结点什么时候delete

左边结点什么时候delete,又取决于右边的_prev什么时候析构,而右边的_prev什么时候析构取决于右边结点什么时候delete

问题又回来了,右边结点什么时候delete,取决于左边的_next什么时候析构......循环往复


weak_ptr

而为了解决循环引用问题,引入了weak_ptr

但是这里的weak_ptr并不是常规的智能指针,它是辅助性智能指针,它没有RAII,也不支持直接的资源管理

weak_ptr主要用shared_ptr构造,用来解决shared_ptr循环引用的问题

红框的部分即用shared_ptr构造weak_ptr

将上述代码改为:

当Node中的_next和_prev是weak_ptr时,不参与资源的释放管理,可以访问和修改资源,但是不增加计数,所以就不会存在循环引用的问题了

此时观察运行结果:

执行完 n1->_next = n2;n2->_prev = n1;后,计数仍为1,所以main函数结束后,n1n2析构,计数--变为0,执行了析构函数,打印了~Node

weak_ptr的简易模拟代码,目的是方便理解:

//辅助型智能指针
template<class T>
class weak_ptr
{
public://无参构造weak_ptr():_ptr(nullptr){}//shared_ptr拷贝构造weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get());{}//weak_ptr本身拷贝构造weak_ptr(const weak_ptr<T>& wp):_ptr(wp._ptr);{}//shared_ptr赋值weak_ptr<T>& operator=(const shared_ptr<T>& sp){//这里不存在自己给自己赋值的场景,所以不需要判断_ptr = sp._ptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
public:T* _ptr;
};

定制删除器

实际中是有可能出现下面这种情况的:

我们普通的shared_ptr的析构函数都是delete,但是如果我们new [],但是却没有配对delete [],少了[],就会出错

我们知道new的底层是malloc,delete的底层是free,我们执行new Node[5],相当于malloc + 5次构造函数,这里的5是从new Node[5]这里的代码中获得的,但是delete []却并没有给次数,不知道需要执行几次析构函数

所以VS的编译器底层在刚刚new的资源的存储位置头部多开了4个字节,用于存储个数,用于告诉编译器需要析构几次

但是我们的指针ptr却仍然指向刚刚的位置,所以实际所开的空间就如下图所示:

所以我们最后执行delete []时,free的并不是ptr的位置,而是ptr减了4个字节的位置,因为我们实际多开辟了四个字节

所以大家就明白了为什么使用delete程序会崩溃,因为delete并不会找头部4个字节所存的次数,即ptr所指向的位置并不是所开空间的起始位置,正确的起始位置应该还要减4个字节,所以释放的位置不对而导致程序崩溃,而delete []则能够往前找4个字节,所以C++语法要求我们new []一定要对应使用delete []

如果是shared_ptr<int> n1(new int[5]);就不会出错,因为只有自定义类型才会调用析构函数,内置类型不需要调用析构函数,因此delete不会出错


所以针对上面的问题,引入了定制删除器的概念

shared_ptr支持定制删除器

unique_ptr也支持定制删除器:

这两个指针支持的方式是有区别的:shared_ptr是在构造函数中支持的,可以在构造时传入对象,而unique_ptr是给的模版参数,传入的是类型

下面先演示shared_ptr的使用方式:

下面的Delete和Free即我们自己实现的定制删除器

分别给n1n3传入匿名对象

此时运行结果:

free后调用一次析构;delete []后,调用5次析构

上面这种方式是传入的仿函数的匿名对象

由于shared_ptr传入的是对象,所以也可以用我们前面所学的lambda表达式,lambda也是对象,所以也可以使用

所以main函数中,也可以这样使用:


下面是unique_ptr的使用,即模版的方式使用:

需要注意的是这里的Delete<Node>后面没有括号,因为unique_ptr传入的是类型,不需要加括号,而刚刚的shared_ptr传入的是对象,需要加括号表示匿名对象


而定制删除器我们如果想简易的模拟一下,只能用unique_ptr的方式模拟实现

多一个模版参数D,我们这里没办法像库里面一样,在构造函数那里实现,因为构造函数那里有一个模版参数D,析构函数无法获得D,并且库里面代码实现的复杂度是远远高于我们自己模拟实现的,所以我们只是模拟实现有助于理解

在析构函数中,创建匿名对象,传入_ptr,传入的定制删除器是delete []就delete [],是free就free

如果是普通的的delete,为了和原来使用方式一样,我们可以写一个默认的删除器,表示不传就默认是delete:


总结几个问题,用于复习智能指针章节的知识:

为什么需要智能指针?

RAII是什么?

智能指针的发展历史?

auto_ptr、unique_ptr、shared_ptr、weak_ptr区别及其使用场景?

模拟实现简易版的智能指针?

什么是循环引用?如何解决?解决的原理?


三、关于内存泄漏

内存泄漏的概念:

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

例如我们一开始所举的例子,new了一个对象,但是抛异常,直接被main函数的catch语句捕获,导致没有delete,造成内存泄漏,即:

但是进程如果是正常结束的,是会释放内存的,那这么说的话,内存泄漏还有没有危害了呢,当然是有的

内存泄漏的危害:

僵尸进程有内存泄漏,如果僵尸进程非常多,就会造成资源被占用很多

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


如何避免内存泄漏:

1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。

需要注意的是:如果碰上抛异常的情况时,就算注意释放了,还是可能会出问题。

2. 采用RAII思想或者智能指针来管理资源。

3. 公司内部规范使用内部实现的私有内存管理库,自带内存泄漏检测的功能选项。

4. 出问题了使用内存泄漏工具检测。

需要注意的是:一般工具不一定能检测出来,亦或是收费较贵


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

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

相关文章

vue中的this.$nextTick().then()

MENU 示例一示例二sortsplicepushrandomfloorMathwhile演示 示例一 let reorganize function (arr){let rest [];while (arr.length > 0) {let random Math.floor(Math.random() * arr.length);// 把获取到的值放到新定义的数组中rest.push(arr[random]);// 这句代码的作…

【Flink】Flink核心概念简述

目录 一、Flink 简介二、Flink 组件栈1. API & Libraries 层2. runtime层3. 物理部署层 三、Flink 集群架构四、Flink基本编程模型五、Flink 的优点 一、Flink 简介 Apache Flink 的前身是柏林理工大学一个研究性项目&#xff0c; 在 2014 被 Apache 孵化器所接受&#xf…

Hive jar包冲突问题排查解决

1、报错情况 hiveserver2启动失败&#xff0c;查看日志报错&#xff1a; 2022-07-04T20:14:53,315 WARN [main]: server.HiveServer2 (HiveServer2.java:startHiveServer2(1100)) - Error starting HiveServer2 on attempt 1, will retry in 60000ms java.lang.NoSuchMethod…

『Charles数据抓包功攻略』| 如何使用Charles进行数据抓包与分析?

『Charles数据抓包功攻略』| 如何使用Charles进行数据抓包与分析&#xff1f; 1 Charles简介2 Charles主要功能3 Charles下载4 Charles安装5 Charles界面介绍6 不能抓取localhost数据的解决方法7 http抓包8 https抓包8.1 SSL证书导入8.2 SSL Proxying Setting设置 9 APP抓包9.1…

Redis高可用之Sentinel哨兵模式

一、背景与简介 Redis关于高可用与分布式有三个与之相关的运维部署模式。分别是主从复制master-slave模式、哨兵Sentinel模式以及集群Cluster模式。 这三者都有各自的优缺点以及所应对的场景、对应的业务使用量与公司体量。 1、主从master-slave模式 【介绍】 这种模式可以采用…

ssm土家风景文化管理平台源码和论文答辩PPT

摘要 土家风景文化管理平台是土家风景文化管理必不可少的一个部分。在风景文化管理的整个过程中&#xff0c;平台担负着最重要的角色。为满足如今日益复杂的管理需求&#xff0c;各类土家风景文化管理平台也在不断改进。本课题所设计的土家风景文化管理平台&#xff0c;使用jav…

FacetWP Relevanssi Integration相关性集成插件

点击阅读FacetWP Relevanssi Integration相关性集成插件原文 FacetWP Relevanssi Integration相关性集成插件是FacetWP与用于高级搜索的 Relevanssi 插件的集成显着增强了您网站的搜索功能。这个强大的工具使您的用户能够轻松找到他们寻求的特定内容&#xff0c;无论他们的查询…

MySQL:找回root密码

一、情景描述 我们在日常学习中&#xff0c;经常会忘记自己的虚拟机中MySQL的root密码。 这个时候&#xff0c;我们要想办法重置root密码&#xff0c;从而&#xff0c;解决root登陆问题。 二、解决办法 1、修改my.cnf配置文件并重启MySQL 通过修改配置文件&#xff0c;来跳…

Course2-Week2-神经网络的训练方法

Course2-Week2-神经网络的训练方法 文章目录 Course2-Week2-神经网络的训练方法1. 神经网络的编译和训练1.1 TensorFlow实现1.2 损失函数和代价函数的数学公式 2. 其他的激活函数2.1 Sigmoid激活函数的替代方案2.2 如何选择激活函数2.3 为什么需要激活函数 3. 多分类问题和Soft…

CSS实现小球边界碰撞回弹

如何通过CSS实现一个物体在屏幕中无限的边界碰撞回弹呢&#xff1f;我们可以使用动画效果实现 代码 我们只做一个小球&#xff0c;通过定位属性叠加动画的方式&#xff0c; 让小球在屏幕中进行运动&#xff0c;通过设置animation的alternate属性来设置回弹。最后&#xff0c;只…

智能优化算法应用:基于人工电场算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于人工电场算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于人工电场算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.人工电场算法4.实验参数设定5.算法结果6.参考…

运维03:LAMP

黄金架构LAMP 什么是LAMP LAMP是公认的最常见&#xff0c;最古老的黄金web技术栈 快速部署LAMP架构 #停止nginx&#xff0c;并且把nginx应用卸载了 systemctl stop nginx yum remove nginx -y#关闭防火墙 iptables -F #清空防火墙规则&#xff0c;比如哪些请求允许进入服…

06 数仓平台MaxWell

Maxwell简介 Maxwell是由Zendesk公司开源&#xff0c;用 Java 编写的MySQL变更数据抓取软件&#xff0c;能实时监控 MySQL数据库的CRUD操作将变更数据以 json 格式发送给 Kafka等平台。 Maxwell输出数据格式 Maxwell 原理 Maxwell工作原理是实时读取MySQL数据库的二进制日志…

C# 热键注册工具类

写在前面 介绍一个验证过的热键注册工具类&#xff0c;使用系统类库user32.dll中的RegisterHotkey函数来实现全局热键的注册。 代码实现 [Flags]public enum KeyModifiers{Alt 1,Control 2,Shift 4,Windows 8,NoRepeat 0x4000}public static class HotKeyHelper{[DllImp…

01、pytest:帮助你编写更好的程序

简介 ​pytest框架可以很容易地编写小型、可读的测试&#xff0c;并且可以扩展以支持应用程序和库的复杂功能测试。使用pytest至少需要安装Python3.7或PyPy3。PyPI包名称为pytest 一个快速的例子 content of test_sample.py def inc(x):return x1def test_ansewer():assert i…

OpenCV-Python:图像卷积操作

目录 1.图像卷积定义 2.图像卷积实现步骤 3.卷积函数 4.卷积知识考点 5.代码操作及演示 1.图像卷积定义 图像卷积是图像处理中的一种常用操作&#xff0c;主要用于图像的平滑、锐化、边缘检测等任务。它可以通过滑动一个卷积核&#xff08;也称为滤波器&#xff09;在图像…

MySQL之时间戳(DateTime和TimeStamp)

MySQL之时间戳&#xff08;DateTime和TimeStamp&#xff09; 文章目录&#xff1a; MySQL之时间戳&#xff08;DateTime和TimeStamp&#xff09;一、DateTime类型二、TimeStamp类型三、DateTime和TimeStamp的区别 当插入数据时&#xff0c;需要自动记录一个时间时候&#xff0c…

人工智能_机器学习059_非线性核函数_poly核函数_rbf核函数_以及linear核函数效果对比---人工智能工作笔记0099

人工智能_机器学习059_非线性核函数介绍---人工智能工作笔记0099 那么我们应该如何调整这个SVC的参数,也就是我们应该使用哪种核函数,比较合适呢?这取决于我们的数据,适合使用哪个核函数,正好我们有 提供的score = accuracy_score(y_test,y_pred) 这样的评分函数,我们可以根据…

保护你的数据:深入了解安全测试!

安全测试是一种非功能性测试。与功能测试不同&#xff0c;功能测试关注的是软件的功能是否正常工作&#xff08;软件做什么&#xff09;&#xff0c;非功能测试关注的是应用程序是否被正确设计和配置。 安全测试的主要目标&#xff1a; 识别资产-需要保护的东西&#xff0c;如…

STM32单片机项目实例:基于TouchGFX的智能手表设计(1)项目介绍及GUI界面基础

STM32单片机项目实例&#xff1a;基于TouchGFX的智能手表设计&#xff08;1&#xff09;项目介绍及GUI界面基础 一、项目介绍 1.1方案提供 1.2主控选择 1.3硬件平台 1.4 开发环境 1.5 关于华清 二、GUI界面基础 2.1.1 嵌入式绘图系统 2.1.1 色彩格式 2.1.1帧缓冲区 …