learn C++ NO.29——智能指针

什么是智能指针?

智能指针是一种C++的类模板,用于自动管理动态分配的内存资源。它们的行为类似于普通指针,但在对象生命周期结束时,会自动调用它的析构函数释放所指向的动态内存资源,从而避免内存泄漏。主要有std::unique_ptr、std::shared_ptr和std::weak_ptr三种智能指针。

为什么需要智能指针?

由于C++的异常处理可能会会导致执行流乱跳,导致动态申请的堆区资源没能及时释放,进而引发内存泄漏。而在没有智能指针之前只能通过在每一个可能引发异常的调用逻辑部分使用try catch语句进行捕获异常并处理相应需要释放的动态内存。但是,人总会犯错。所以,为了解决这个问题,就有大佬提出了RAII 机制来保证异常的安全。

RAII(Resource Acquisition Is Initialization)即 "资源获取即初始化",是 C++ 语言中的一种编程技术和资源管理理念。其核心思想是将资源的获取和释放与对象的生命周期绑定在一起。

下面通过一个简单的场景来看一下:

上面的代码中,每一次new都有抛异常的风险,虽然这个风险微乎其微。但是难免会出现异常。所以保证f()函数异常安全的代码及其复杂,需要考虑每一次new失败的异常处理,这样的代码会逼疯程序员的。而引入RAII机制后,将动态内存用一个类进行托管,在构造中保存一份动态内存的起始地址。在类的析构函数中显示调用delete处理动态申请的内存,由于类的生命周期结束会自动调用析构函数清理资源。这样就可以有效避免异常诱发的内存泄漏问题。


这里模拟实现的智能指针的方式也称为RAII编程。RAII即资源获取即初始化,是一种C++的编程技术以及资源管理的方式。核心思想是将资源的初始化和释放绑定在对象的生命周期中,当对象被创建就用它的数据来初始化这个模板类,当对象生命周期结束,编译器会调用析构来清理申请的资源。

为了让智能指针像指针一样被使用,所以还需要提供operator* 和 operator->的重载。

下面来看一个经典错误,由于浅拷贝所导致的析构两次。最终程序崩溃的场景。那么我们需要提供深拷贝吗?答案是不用的,因为指针是内置类型,对于内置类型只需要提供浅拷贝。再者难道就无解了吗?那我们就需要看看委员会大佬们是如何解决这个问题的。

auto_ptr

auto_ptr是C++98引入的一种智能指针。它可以用于自动管理动态申请的对象。使用它需要包含头文件 < memory>

由于auto_ptr的拷贝工作原理是管理所有权转移,即被拷贝对象将自己的资源的管理所有权转移给拷贝对象。由于被拷贝的对象悬空,若此时代码编写不规范对悬空指针进行操作就会导致程序崩溃。

由于auto_ptr拷贝会导致悬空问题,所以实践中很多项目都是严禁使用它。并且它带来的争议还是不小的,一些老牌程序员对它可是"恶意满满"。

下面简单手撕一份demo版本的auto_ptr来加深对它的了解。首先,需要保证RAII机制,然后需要提供operator * 和 operator ->的重载让它能像指针一样被使用。最后实现一份拷贝构造的demo 代码,让它获取被拷贝对象的资源,以及将被拷贝对象置空。

unique_ptr

由于auto_ptr在实践场景中的缺陷比较大,所以在C++11出来之前,一些委员会的大佬们通过boost库放出来了一些完善过后的智能指针,比如scoped_ptr、shared_ptr以及weak_ptr等。让C++社区的程序员去使用。到了C++11标准出来,支持了unique_ptr、shared_ptr以及weak_ptr。

介绍一下unique_ptr,它是一种禁止拷贝的智能指针。

下面就模拟实现一份unique_ptr的代码

unique_ptr的面对拷贝问题的处理方式简单粗暴,既然拷贝会有问题,那就直接禁止拷贝。在智能指针不涉及拷贝的场景下,可以使用unique_ptr。

shared_ptr

shared_ptr允许拷贝,它底层采用引用计数的方式,允许多个shared_ptr只想同一块空间。当调用析构时引用计数为零就释放维护的动态空间和计数器。计数器不为零就将计数器存的值- 1。

下面模拟实现一份shared_ptr。首先,需要考虑引用计数的实现方式,需要采用new块空间进行维护,因为不同的shared_ptr可能共同使用这个计数器。在构造函数调用时,需要开辟这个计数器的空间。调用析构需要判断计数器当前是否只被一个shared_ptr使用,若是就要释放动态资源。否则就是--计数器的值。在拷贝构造和拷贝赋值重载的实现上需要注意将计数器的地址给拷贝过去,并且++计数器。

对于拷贝赋值重载需要注意自己给自己复制的场景,以及修改指向需要让原来指向的空间的引用计数--,并判断是否需要释放。否则就会内存泄漏。

参考代码:

看似好像shared_ptr很完美啊,既能RAII,有解决了智能指针拷贝的问题。事实真是这样的吗?share_ptr由于支持引用计数,在多线程场景下需要保证++引用计数 和 --引用计数操作的原子性。并且多线程场景下还会存在多个线程同时访问同一块堆区资源,进而导致数据竞争、数据破坏以及死锁等问题。

下面就来看一个shared_ptr循环引用的场景,让大家看清它的看似完美背后的缺点。

下面分析一下这个问题,首先是让sp的_next指针连接sp2,此时sp2引用计数为2。再让sp2的 _prev指针指向sp1,此时sp1的引用计数为2。程序结束即将,编译器自动调用析构函数清理。此时,循环引用的场景出现。下面通过画图带大家看一看。

走完正常析构逻辑后,sp1和sp2都被析构。但是,由于_prev和_next造成了循环引用,所以两块空间以及两个计数器都无法正常释放,引发了内存泄漏。这里分析一下循环引用的成因。由于_prev所在的空间正在被_next指向,引用计数为1,所以无法释放。由于_next所在的空间正在被_prev指向,引用计数为1,所以也无法释放。这就造成了析构逻辑的"死循环"了。那如何解决呢?就需要请出weak_ptr了。

weak_ptr

weak_ptr专门用于解决shared_ptr循环引用的缺陷。它不是一个RAII的智能指针。weak_ptr底层不会进行引用计数,它可以访问shared_ptr指向的资源,但它不管理资源。

下面就手撕一份weak_ptr的demo代码。由于不支持RAII的特性,所以不需要在析构时对资源进行释放。由于需要支持shared_ptr的拷贝构造和复制重载,所以需要再shared_ptr提供一个get接口以获取动态资源。参考代码如下图:

定制删除器

由于申请空间的方式有多种,比如new、new[]、malloc等等方式。但是,我们在实现 shared_ptr是忽略了一些关于别的申请动态内存方式的处理逻辑。这会导致开辟空间和析构空间的接口不匹配,而这种不匹配让程序造成后果是标准未定义的高危行为。

对此我们可以提供一个定制删除器来解决这个问题。库里面提供的方式是给一个模板类的构造函数来解决的。

下面就通过多定义一个包装器成员变量来接收用户在构造时传的可调用对象,用这个可调用对象来析构。

不仅shared_ptr有这个问题,unique_ptr也有这个问题,不过unique_ptr和shared_ptr在处理方式上有些许不同。unique_ptr支持在类模板外部传可调用对象。

内存泄漏问题

内存泄漏指的是程序在申请堆区内存资源时,在正常使用逻辑结束后,没有进行手动释放申请的资源,导致系统内存的浪费。内存泄漏还可能是申请某些操作系统资源,使用后没有释放导致,如申请文件描述符没有释放、管道资源没有释放以及锁资源每有释放都会导致内存泄漏。

内存泄漏的危害就是会导致整个程序卡顿、程序异常终止。更严重可能会导致操作系统崩溃。相应的在实际生活中,由于通常C++程序员都是写后端程序较多,若是后端程序异常相应的会导致企业的损失。举个我身边的例子,去年的某一天阿里云的服务端的服务端崩溃了,导致那天中午我们学习的外卖系统全部无法正常接单。并且这件事还引起很大层面的社会影响,还上了当天的热搜。虽然具体原因未知,但是后端程序的内存泄漏就会引发类似这样的问题。

内存泄漏的解决方式有很多,但个人认为最重要的一点就是事前预防。在设计程序的时候要避免,写代码时要规范。比如说合理的使用智能指针、确认资源所有权、使用成熟的第三方库以及严格的进行测试等等。

相关推荐
小魏冬琅8 分钟前
探索面向对象的高级特性与设计模式(2/5)
java·开发语言
lihao lihao11 分钟前
C++stack和queue的模拟实现
开发语言·c++
TT哇22 分钟前
【Java】数组的定义与使用
java·开发语言·笔记
天天进步201527 分钟前
Lodash:现代 JavaScript 开发的瑞士军刀
开发语言·javascript·ecmascript
姆路36 分钟前
QT中使用图表之QChart概述
c++·qt
假装我不帅36 分钟前
js实现类似与jquery的find方法
开发语言·javascript·jquery
look_outs40 分钟前
JavaSE笔记2】面向对象
java·开发语言
萧鼎41 分钟前
【Python】高效数据处理:使用Dask处理大规模数据
开发语言·python
西几1 小时前
代码训练营 day48|LeetCode 300,LeetCode 674,LeetCode 718
c++·算法·leetcode
风清扬_jd1 小时前
Chromium HTML5 新的 Input 类型week对应c++
前端·c++·html5