c++ 智能指针

为什么要有智能指针

在上篇异常安全问题那里有如下的样例

对于可能存在内存泄漏的问题使用了异常的重新抛出来解决 这种方式确实解决了这里的问题 但是这种方法感觉有些拉了

而且更重要的是有些问题是无法解决的 如果new也抛异常呢?

所以此时智能指针就该发挥它的价值了

RAII和智能指针的设计思路

RAII是ResourceAcquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏。在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常 释放,避免资源泄漏问题

那么为什么这么做呢 为什么这样子就能解决这样的问题了呢

在上篇异常的捕获和抛出那里有一个关于string类的例子 虽然抛异常之后会直接跳转到catch捕获的地方 但是函数的栈帧还是会正常销毁的 会沿着调用链创建的对象都将销毁。

所以对于上面自定义的智能指针类对象 会自动调用他们的析构函数把动态申请的资源是释放掉 就把这样的问题解决了

此外智能指针类还要方便资源的访问,所以智能指针类还会想迭代器类⼀ 样,重载 operator*/operator->/operator[] 等运算符

智能指针的问题

如下 s2用sp1进行构造 此时进行的是拷贝 两者里面的指针是相同的 指向同一块空间 在调用析构时候 同一块空间就被析构了两次

那么如果我们实现深拷贝呢?

深拷贝肯定是可以解决析构两次的问题 但是我们要从里面存的内容的拷贝的意义来考虑 此时的智能指针相当于是对里面指针内容的封装 如果用里面指针内容来构造一个新对象 我们期望的就应该是深拷贝的结果即两者指向一块空间 只不过因为用智能指针封装了一层的原因导致了这个析构两次的问题 所以我们不能考虑深拷贝

所以我们要在浅拷贝的基础上解决两次或者多次析构的问题

在C++98时设计出auto_ptr智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是⼀个不好的设计,因为他会到被拷贝对象悬空,访问报错的问题

例如 下面有两个Date的类 先创建了auto_ptr智能指针类型的p1 然后p2用p1来构造 此时只析构了一次 确实解决析构多次的问题了

但是这种方式会把p1直接给置空(类似与移动构造 但是移动构造是对右值临时对象的) 此时使用p1相当于空指针访问了 就出错了

所以auto_ptr被很多人骂 很多公司也禁止使用

直到c++11有了unique_ptr shared_ptr weak_ptr三个智能指针才真正解决了这样的问题 C++标准库中的智能指针都在<memory>头文件下面,我们包含就可以是使⽤了

unique_ptr

unique_ptr 名字翻译出来是唯⼀指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就建议使用

它把拷贝给禁止了 只支持移动的 进行拷贝构造会报错 移动构造可以正常进行 但是移动的情况和之前auto_ptr不还是一样吗 还是会把原来的对象p1内容给置空

是的 但是它避免了我们的误操作 我们使用auto_ptr可能只是使用错了 不小心把之前的内容给置空了 而对于unique_ptr对于左值会直接报错 而如果我们move之后我们使用者应该是清楚被拷贝的内容会被清空

shared_ptr

shared_ptr名字翻译出来是共享指针,他的特点是支持拷贝, 也支持移动 。如果需要拷贝的场景就需要使用他了。底层是⽤引用计数的方式实现的

weak_ptr

weak_ptr翻译出来是弱指针,他和上面的智能指针完全不同,他不⽀持RAII,也就意味着不能用它直接管理资源,weak_ptr的产⽣本质是要解决之后提到的(shared_ptr 的循环引用导致内存泄漏的问题)

智能指针的原理

auto_ptr和unique_ptr这两个智能指针的实现比较简单,了解⼀下原理即可。auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr的思路是不支持拷贝。

重点在于shared_ptr的实现 shared_ptr用到了引用计数的方式

那么引用计数在类里面用什么方式实现呢

这个n要求的是指向同一块资源的对象个数 首先下面这种方式绝对是不行的

用静态的方式也是不可以了 这样所有的对象都会共用一个n

要使用堆上动态开辟的方式 如果是调用构造 此时说明是新的资源 动态开辟一块空间专门存个数 调用的是拷贝构造 则不重新开辟 直接把内容拷贝 然后n指向的个数++

shared_ptr实现

拷贝赋值的问题

拷贝赋值要注意下面的问题

在shared_ptr类里面实现的析构调用的都是delete的方式(这里用的是库里的shared_ptr)

但是如果构造时候用的是 new []的方式呢 此时就发生问题了 因为new[] 需要用delete[]来析构

在模版参数类型后面加[] 此时会正常析构 其实是底层实现特化的原因 因为new[]很常见 所以unique_ptr和shared_ptr专门会特化处理这样 当构造时候用的是new[] 再析构的时候就会调用delete[]来析构

但是如果是文件操作或者malloc呢 此时析构时候需要用fclose或者free

所以此时需要有更通用的方式--------删除器 (其实就是提供第二个参数为仿函数对象)

第二个参数可以是仿函数类对象 可以直接用lambda的方式 也可以用函数指针的方式

unique_ptr和shared_是需要再模版参数里面写类型

所以对于shared_ptr 使用删除器 建议使用lambda或者仿函数的方式

而unique_ptr使用删除器的时候 建议使用仿函数的方式

定制删除器的实现

我们这里要实现的删除器是shared_ptr定制删除器的方式

再写一个构造函数 处理两个参数的情况 即需要手动传递删除器的情况 默认不需要手动提供删除器的就用一个参数的构造函数

在有删除器的构造函数中我们要把传过来的这个删除器来保留下来 所有我们还需要一个成员变量_del 但是这个成员变量的类型需要也为D 但是这个D只支持这个构造函数 如果在成员变量也支持 需要D和T模版参数放到一起

那么这样的话 在创建对象的时候我们需要给模版参数传两个参数如果实现了缺省值后需要一个参数 需要手动提供删除器的话要往模版里面传 那这样就是unique_ptr删除器的实现方式了

所以对于del这个成员变量用下面包装器的方式来实现 这样可以接收lambda可以是仿函数 可以是函数指针 另外为了构造函数一个参数的情况 这里缺省值给上lambda的方式 处理不需要自己提供删除器的情况

然后把需要释放_ptr的地方都修改一下 改成_del(_ptr)

此时把std的shared_ptr换成我们自己实现的 可以发现也是按照我们预期正常运行没有崩溃

make_shared

shared_ptr还支持make_shared来构造 下面这两种方式使用上是完全一样的

区别在于

第一种方式 会先new一次 在堆区开一次空间 在shared_ptr构造的时候又会为计数n开一次空间

而第二种方式是先在make_shared用数据进行了构造 还没有开空间 然后再shared_ptr一下把ptr需要的空间和计数n需要的空间一起开了 此时这两块空间是连续的

所以make_ptr相对于第一种方式会让开的空间更连续 所以只有在空间资源很多的时候才会有区别

shared_ptr和unique_ptr都支持了operator bool的类型转换,如果智能指针对象是⼀个 空对象没有管理资源,则返回false,否则返回true

shared_ptr和unique_ptr的构造函数都使⽤explicit修饰,防止普通指针隐式类型转换成智能指针对象。

shared_ptr循环引用问题

先搞一个非常简易的链表

所以链表里面存shared_ptr<ListNode>类型可以解决这个问题

当把pt1的next指向pt2 pt2的prev指向pt1后会发生下面的问题

那么为什么会出现这样的问题呢

下面是初始情况

在把pt1的next指向pt2 pt2的prev指向pt1后

此时析构时候会调用pt2和pt1的析构 对于pt2 因为它的_n指向的计数个数--后不为0 所以不会析构里面的资源 pt1也是一样的 所以所以两个里面的ptr n和ListNode都没有释放造成了资源泄漏

对于pt2存的的资源 除了它之外还有pt1的next也存着 所以要把它完全释放 需要先把pt1的next也给析构一下

而pt1的next是pt1里面的资源 需要先把pt1给释放掉

而pt1存的的资源 除了它之外还有pt2的prev也存着 所以要把它完全释放 需要先把pt2的prev也给析构一下

而pr2的prev又是pt2里面的资源 需要先把pt2给释放掉

所以他们就构成了这样的无限循环

而要解决这样的问题就要用到weak_ptr

如下 链表里面里面存的是weak_ptr就可以解决这样的问题了

那么为什么呢

weak_ptr可以用shared_ptr直接构造

并且weak_ptr绑定到shared_ptr时不会增加它的引用计数 只是和其指向同一块资源 它的析构不会把里面指向的资源给释放 只是把里面存的指针给释放掉而已 并且其不支持重载运算符也不能对指向的资源直接使用 (库里面的shared_ptr和weak_ptr实际上要复杂的多 这里只是简易实现)

实际上的weak_ptr里面不是只有里面指针 它也会指向引用计数 不然如果shared_ptr把里面的资源给释放了 但是它不知道 就进行空指针访问了

如下因为有指向计数的指针 所以可以统计个数和判空 (实际上的底层是把引用计数的指针专门写做了一个类 shared_ptr在析构的时候 只会释放ptr不会是否引用计数的空间 它有weak_ptr来释放(如果shared_ptr指向的空间也被weak_ptr指向的话))

通过lock()尝试获取一个临时的shared_ptr来访问对象

如果关联的对象仍然存在(shared_ptr引用计数 > 0),则返回一个新shared_ptr,并增加引用计数。

如果对象已被销毁,返回空的 shared_ptr。

相关推荐
Code_Shark3 小时前
AtCoder Beginner Contest 424 题解
数据结构·c++·算法·数学建模·青少年编程
今天又在学代码写BUG口牙3 小时前
MFC应用程序,工作线程学习记录
c++·mfc·1024程序员节
j_xxx404_3 小时前
C++ STL简介:从原理到入门使用指南
开发语言·c++
15Moonlight3 小时前
06-MySQL基础查询
数据库·c++·mysql·1024程序员节
Dream it possible!3 小时前
LeetCode 面试经典 150_链表_反转链表 II(60_92_C++_中等)(头插法)
c++·leetcode·链表·面试
懒惰蜗牛4 小时前
Day44 | J.U.C中的LockSupport详解
java·开发语言·后端·java-ee
闲人编程4 小时前
Python设计模式实战:用Pythonic的方式实现单例、工厂模式
开发语言·python·单例模式·设计模式·工厂模式·codecapsule·pythonic
Moniane4 小时前
API技术深度解析:从基础原理到最佳实践
开发语言
风已经起了4 小时前
FPGA学习笔记——用Vitis IDE生成工程(串口发送)
笔记·学习·fpga开发·fpga·1024程序员节