初识C++之智能指针

news2024/9/8 7:18:33

目录

一、智能指针的概念

二、RAII

三、 智能指针的拷贝构造

1. 智能指针的拷贝构造问题

2. C++库中的智能指针

2.1 auto_ptr

2.2 unique_ptr

2.3 shared_pt

2.4 weak_ptr

四、shared_ptr的循环引用问题

五、 定制删除器


一、智能指针的概念

在了解智能指针的概念前,先写出如下程序:

在这个程序里面,Div函数会抛出一个异常,func函数会捕获这个异常并将这个异常继续抛出给main函数。注意,在这个程序里面的func函数new了一个空间,在catch中释放了这块空间。此时只new了一块空间。

但是,如果在这个程序中的func函数中再new一个空间。此时就需要释放两个空间。有人可能会认为多开一块空间没什么,只需要在catch中新增一条delete语句即可:

但是,这里大家忽略了一个问题,那就是new其实也是可能抛异常的。比如当new的空间过大,内存无法满足的情况下, new就会抛出一个异常。因此,在新创建了一个空间后,也需要对这块空间进行捕获:

这种写法首先看起来很难看。并且最重要的是,由于new可能抛异常,所以如果再增加一个p3,就需要再套一层try catch。随着new的空间越多,就需要套越多层的try catch。这种写法无疑是非常麻烦且难看的。

面对这种情况时,就可以使用智能指针。提供如下一个类:

 有了这个类后,就可以不再需要在捕获中释放空间了。修改func函数:

在这里,将p1和p2交给SmartPtr对象,让这个类中的_ptr指向对应的空间。这两个类在这里属于临时变量,一旦出了作用域就会结束。因此,当Div函数出现异常时,它会直接跳转到main函数的catch中,此时func函数的生命周期结束,也就带着这两个SmartPtr对象销毁了。而这两个对象在销毁时会调用析构函数用delete销毁new出来的空间

通过上面这种方式,就可以“将new出来的空间的生命周期与一个局部对象的生命周期相绑定”,实现自动释放的功能。也就无需再为了防止Div函数出错而使用catch捕获异常以便于在catch中释放对应的空间。

当然,为了方便,也可以将func中的代码简写:

既然这个类叫做只能指针,当然也需要有像指针一样的功能,所以在类中添加如下内容:

然后写下测试代码并运行测试:

运行正常。

上面的这种将资源生命周期与对象生命周期相绑定的方法,其实就是RAII。总结起来,智能指针的原理其实就是使用RAII的特性和重载了*与->,具有像指针一样的行为

二、RAII

RAII(Resource Acquisition is Initialization),翻译过来就是“资源申请即初始化”。就是一种“利用对象声明周期来控制程序资源”(如内存、文件句柄、网络连接、互斥量等)的简单技术。

简单来讲,RAII就是“在对象构造时获取资源”,控制资源的访问并使之在这个对象的声明周期内始终保持有效。最后“在对象析构时释放资源”。因此,这种技术实际上就是将一份资源交给了一个对象管理。

这种做法有两个好处:

(1)不需要显式地释放资源。因为创建出来的对象都是局部对象,出了作用域自动调用析构函数销毁,也就将管理的资源一并销毁了。

(2)这种方式,将申请的空间的生命周期与对象的生命周期绑定,便于用户更好的管理资源。

三、 智能指针的拷贝构造

1. 智能指针的拷贝构造问题

现在有如下一个我们自己写的智能指针:

写出如代码:

运行上面的代码:

此时就出现了报错。原因很简单。在这个智能指针的类中并没有写拷贝构造,因此编译器会自动生成一个进行浅拷贝的拷贝构造函数,此时sp1和sp2指向同一块空间,在程序结束析构的时候就析构了两次,导致程序错误。

其实智能指针中的RAII和“像指针一样”这两个特性都是非常简单的,并没有什么难度。智能指针真正的问题之一,就在于这个拷贝构造上。

智能指针的行为其实就是在模拟原生指针的行为,所以这里的拷贝构造其实就是要让两个指针指向同一个位置

2. C++库中的智能指针

2.1 auto_ptr

在C++98中,其实就已经提出了智能指针的概念。第一个智能指针是“auto_ptr”

 C++98中的auto_ptr针对拷贝构造提出的解决方案就是“资源管理权转移”。简单来讲就是在一个智能指针在拷贝另一个智能指针后,会将原指针指针的资源转移给新的智能指针,然后将自己置空。写如下代码进行测试:

运行该程序:

可以看到,sp1被置为了空,而sp1的资源被转移到了sp2中。这个智能指针的解决方案有一个很大的问题,就是会导致“悬空”问题:

例如如上代码,如果是一个不清楚这个特性,或者说在使用时没注意到这个问题的人就可能错误的使用sp1这个已经被置空的指针,进而出现错误。

如果我们想实现这一特性,也非常的简单,就是将资源交换然后将原指针置空即可:

一般来讲,在实际使用中是非常不推荐使用auto_ptr的

2.2 unique_ptr

unique_ptr其实是C++委员会从boost库中抄来的,包括下面的shared_ptr和weak_ptr也是如此。boost库可以看做C++标准库的一个预备库,是由C++委员会发起建立的,里面的很多内容在未来都可能进入C++标准库。

unique_ptr解决拷贝构造的方法就很粗暴,从名字“唯一指针”上就可以看出来,它的解决方案就是“禁止拷贝构造”。写入如下代码进行测试:

运行该程序后,可以看到如上报错。表示使用了已经删除的函数。这就可以证明,unique_ptr其实就是禁止了对智能指针的拷贝构造。实现方式也很简单,直接使用delete关键字即可:

带有这个关键字的类中的默认成员函数会被禁止生成和使用。

2.3 shared_pt

上面的unique_ptr是禁止拷贝,但如果我们就是想让两个不同的指针指针指向同一块空间呢?此时就可以使用shared_ptr。从名字“共享指针”就可以看出来,这个智能指针是允许不同的智能指针指向同一块空间的。写出如下代码测试:

 运行程序查看监视窗口:

 可以看到,sp1和sp2指向的是同一块空间。

shared_ptr对拷贝构造的解决方案就是“计数器”

shared_ptr通过计数器的方式记录某块空间有几个智能指针指向,每多一个就增加计数器,在析构时,先--计数器,如果计数器不为0,则不释放空间;如果计数器为0,则释放空间。

那么如何实现这个计数器呢?有的人可能就想,既然要让不同的智能指针看到同一块空间,就可以定义一个静态成员变量,这样就可以解决问题:

但是要知道,static成员是整个类(类所实例化的所有对象)共享的。这也就是说,确实指向同一块空间的智能指针能看到同一块空间。但是,指向不同空间的智能指针也是看到的同一个计数器。如果出现有三个智能指针指向同一块空间,此时计数器为3;但是此时又出现一个智能指针指向其他空间,由于看到的是同一个计数器,所以此时计数器++,变为4。很明显不满足需要。

因此,智能指针的计数器必须让指向同一个空间的智能指针看到同一个计数器;指向不同空间的智能指针看到不同的计数器。

要实现这一方法也很简单。首先定义一个计数器变量,在构造函数中单独为这个计数器new一块空间。此时这个变量的值就存在于堆上。不会因为某个对象结束而被销毁。当要进行拷贝构造时,首先++被拷贝对象的计数器。再让要拷贝的对象的计数器指向被拷贝的计数器,此时它们看到的就是同一个计数器。实现起来也非常简单:

实现了拷贝构造后,再来实现赋值。如果是指向同一块空间的指针赋值,就什么都不需要做。但如果是指向不同空间的指针赋值,首先就需要--原智能指针的计数器;如果计数器为0,还需要释放空间。如果不为0,就要将被赋值的智能指针指向的空间和计数器指向赋值的智能指针,最后再++被赋值智能指针的计数器: 

要实现起来,就比拷贝构造复杂一点:

2.4 weak_ptr

weak_ptr并不是单独使用的,它需要配合shared_ptr,主要用于解决shared_ptr的循环引用问题。这个智能指针主要用于提供对shared_ptr的拷贝构造,甚至不允许带参构造:

要实现起来也是比较简单的:

至于这个weak_ptr如何解决循环引用的问题, 就放在下面讲。

四、shared_ptr的循环引用问题

shared_ptr是一个支持多个智能指针指向同一块空间的类。这个智能指针的多方面都很好用,但有一个很严重的问题存在,就是“循环引用”问题。

写出如下程序:

该程序可以看成一个简化版的链表,每个数据块中只有两个链接上下数据块的节点。创建两个节点,让这两个节点链接起来。运行程序:

此时可以发现,当这个程序结束后,什么都没有打印。但是我们自己写的析构函数中是加了一句话的。既然这里没有打印,也就说明在这个程序结束后,没有调用析构函数释放空间。

我们屏蔽掉一个节点指向后再运行程序:

可以看到,当对一个节点指向屏蔽后,就可以正常调用析构函数了。但是,如果是向上面那样两个节点互相指向,却无法析构。

上面的代码中所用的是我们自己写的shared_ptr,那么这是不是我们自己写的代码有问题呢?换成库中的shared_ptr试试:

要注意,库中的构造函数是加了explicit关键字的,禁止隐式类型转换。所以这里不能使用=创建n1和n2。运行该程序:

可以看到,在两个节点互相指向的情况下,库中shared_ptr也无能为力,无法调用析构函数。同样的,隐藏一个节点指向后运行程序:

同样的,此时又可以正常调用析构函数了。

这种节点互相指向导致无法析构的情况,就叫做“循环引用”问题。

原理很简单,假设有n1和n2两个节点,这两个节点互相指向。而shared_ptr中是存在计数器的,这也就意味着当这两个节点互相指向的时候,n1和n2的计数器都会++变为2。当要析构时,首先析构n2,将n2的计数器--为1,但是此时并没有释放空间,因为n1中还有一个shared_ptr,即_next指向n2;于是接着释放n1,--n1的计数器为1,此时n1也没有释放,因为n2中的有一个shared_ptr,即_prev指向n1。此时就会出现要释放n1,就必须释放n2中的_prev;要释放n2,就要释放n1中的_next的情况。两个节点互相等待对方的释放,导致双方都无法释放。

那么如何解决这个问题呢?很简单,只需要在指向空间时不要++计数器即可。但是shared_ptr是无法自行做到这件事的,所以,库中便提供了weak_ptr来专门处理这种情况:

 修改程序如下:

可以看到,此时依然是存在两个指针互相指向的情况。运行该程序:

可以看到,程序可以正常析构。

至于这个weak_ptr如何模拟实现,在上文中已经讲解过,这里就不再赘述。换成我们自己写的weak_ptr来测试程序:

同样可以正常析构。当然,库中的实现还考虑了很多问题,实现的复杂程度要比我们自己实现的复杂的多,但单个智能指针的实现思想是一样的。

五、 定制删除器

大家知道,在C++中提供了new来申请空间。而new申请空间时,有两种申请方式。一种是不带[]申请,只有一块固定空间;带[],则可以指定申请对应大小的空间。这两种空间的删除方式并不一样。错误使用可能会带来严重后果

如果是内置类型,使用错误的删除方式可能还没有问题:

但如果是自定义类型,使用错误的方式就可能出现问题:

此时就有一个问题了,在智能指针中,如何得知应该使用哪种方式释放空间呢?

此时,就需要使用定制删除器,指定释放空间的方式。

库中的shared_ptr中也是有定制删除器的,其实就是提供仿函数:

例如下图:

在这里,不仅可以正常传仿函数,也可以传lambda表达式。这里大家可能就会比较奇怪了,在以前传仿函数时,都是在类型名处传仿函数名,为什么库中的却是在构造对象的地方,即构造函数中传可调用对象呢?这其实就和C++库中shared_ptr的底层实现有关。

在这里,我们是无法实现像库中这样实现定制删除器的。因为库中其实套了很多个类,通过这些类的嵌套来实现让shared_ptr拿到这个可调用对象。如果单单对构造函数进行修改是没有用的:

在构造函数中单独加一个模板,虽然可以将函数对象传进去,但是要知道,在这里我们并不是要让构造函数使用这个删除器,而是要让shared_ptr使用这个删除器。更准确点,是让shared_ptr的析构函数使用。如果单单给类中的构造函数加一个参数模板,如何让整个类拿到呢?很明显,是无法实现的。库中为了实现这一方法,就采用了多个类的嵌套实现这一操作。实现起来是非常复杂的,这里就不过多讲解。

但是我们要使用定制删除器也是有方法的,那就是给整个类加上一个参数模板即可。

通过传仿函数的方式,就可以让智能指针内部拿到对应的释放空间的方式。但是这种方式有一个缺点,那就是无法使用lambda表达式

因为lambda表达式是一个可调用对象,但是新增参数模板的方式是要在模板中填入类型,所以无法使用lambda表达式。有人可能就会想到使用decltype来声明这个表达式是一个类型,同样是无效的。因为decltype是运行时推导,而这里传入的类型是要在编译时就传入,所以decltype失效

当然,不仅shared_ptr是这样,unique_ptr其实也是一样的,都是经过多个类的嵌套实现了在构造函数中传入释放资源的方法。

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

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

相关文章

Java连接与操作Perforce

对于源码控管的基本使用来说, 使用Perforce的客户端工具就可以了, 但是某些应用场景下可能需要使用代码来与Perforce服务器进行交互, 比如: 自动部署流程中的自动取代码(该场景一般也可以使用P4命令行工具实现&#x…

Windows共享内存与死锁

实验一 一、实验内容或题目: 利用共享内存完成一个生产者进程和一个消费者进程的同步。 二、实验目的与要求: 1、编写程序,使生产者进程和消费者进程通过共享内存和mutex来完成工作同步。 2、了解通过操作系统接口调用,实现通…

Linux字符设备驱动

前言 代码结构简单,旨在用最简单的原理理解最主要的框架逻辑,细节需要自行延伸。 -----------------学习的基础底层逻辑 基础步骤 开发linux内核驱动需要以下4个步骤: 编写驱动代码编写makefile编译和加载驱动编写应用程序测试驱动 由于硬…

Android9.0 系统Framework发送通知流程分析

1.前言 在android 9.0的系统rom定制化开发中,在systemui中一个重要的内容就是系统通知的展示,在状态栏展示系统发送通知的图标,而在 系统下拉通知栏中展示接收到的系统发送过来的通知,所以说对系统framework中发送通知的流程分析很重要,接下来就来分析下系统 通知从frame…

开发攻城狮必备的Linux虚拟机搭建指南|原创

hi,我是阿笠! 这篇文章主要面对的是不常搭建Linux操作系统环境的开发同学,文中介绍了基本操作步骤并且提供了相关云盘资源,都是为了节约时间! 因为从我自身来讲,作为一名后端开发,经常需要练习一…

c#笔记-内置类型

内置类型 内置类型是一些有关键字表示的类型。关键字具有非常高的优先级,可以让你在没有别的配置的情况下, 只要用的是c#就可以使用。这也意味着这些类型是非常重要,或是基本的东西。 整数:byte, sbyte, short, ushort, int, ui…

【Python入门】搭建开发环境-安装Pycharm开发工具

前言 📕作者简介:热爱跑步的恒川,致力于C/C、Java、Python等多编程语言,热爱跑步,喜爱音乐的一位博主。 📗本文收录于Python零基础入门系列,本专栏主要内容为Python基础语法、判断、循环语句、函…

【数据结构】线性表之单链表(讲解实现——带动图理解)

文章目录 单链表单链表主体结构单链表操作函数介绍单链表操作函数实现单链表的初始化:打印函数单链表插入函数:头插尾插指定结点后插入和查找函数单链表结点之前插入数据 单链表删除函数头删尾删指定结点后删除指定结点删除 销毁单链表 文件分类test.cLi…

【STM32】基础知识 第十课 CubeMx

【STM32】基础知识 第十课 CubeMx STM32 CubeMX 简介安装 JAVACubeMX 安装新建 STM32 CubeMX 工程步骤新建工程时钟模块配置GPIO 配置生成源码 main.c STM32 CubeMX 简介 CubeMX (全称 STM32CubeMX) 是 ST 公司推出的一款用于 STM32 微控制器配置的图形化工具. 它能帮助开发者…

「Bug」解决办法:Could not switchto this profil,无法使用节点的解决方法,彻底解决

♥️作者:白日参商 🤵‍♂️个人主页:白日参商主页 ♥️坚持分析平时学习到的项目以及学习到的软件开发知识,和大家一起努力呀!!! 🎈🎈加油! 加油&#xff01…

二十五、OSPF高级技术——开销值、虚链路、邻居建立、LSA、静默接口

文章目录 调试指令(三张表)1、邻居表:dis ospf peer brief2、拓扑表(链路状态数据库):dis ospf lsdb3、路由表:dis ip routing-table 一、OSPF 开销值/度量值(cost)1、co…

Python基础合集 练习15(内置函数 匿名函数)

匿名函数 以lambda开头表示这是匿名函数,之后的x,y是函数参数 def sub(a,b): return a-b print(sub(10,3)) print(lambda x,y:x-y) sublambda x,y:x-y print(sub(8,4)) def game(math,chinese,english): “”" 功能:计算三科的成绩 math&#xf…

谈谈多线程的上线文切换

大家好,我是易安! 我们知道,在并发程序中,并不是启动更多的线程就能让程序最大限度地并发执行。线程数量设置太小,会导致程序不能充分地利用系统资源;线程数量设置太大,又可能带来资源的过度竞争…

【C++】隐式转换与explicit关键字、运算符及其重载、this关键字

C隐式转换与explicit关键字 隐式构造函数 隐含的意思是不会明确告诉你要做什么 隐式转换 C允许编译器对代码执行一次隐式转换&#xff0c;而不需要使用casr强制转换 例1 #include <iostream> #include <string>class Entity { private:std::string m_Name;in…

13 SQL——数值函数

1 ceil() 数值向上取整&#xff08;前提是小数位不是0&#xff09; select ceil(1.2);2 floor() 数值向下取整&#xff08;前提是小数位不是0&#xff09;select floor(1.8);3 mod() 取&#xff08;x%y&#xff09;的模运算&#xff08;求余数运算&#xff09; select …

10. hr 综合面试题汇总

10. hr 综合面试题汇总 C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅) 本章讲解知识点 1.1 HR心理复盘1.2 HR常问问题——学校的表现怎么样啊?1.3 HR常问问题——了解我们公司吗?1.4 HR常问问题——个人情况1.5 HR常问问题——业余生活1.6 HR常问问题——薪资待…

【源码角度】为什么AQS这样设计

AQS&#xff08;AbstractQueuedSynchronizer&#xff0c;抽象同步队列器&#xff09;是 一个基于 FIFO的双端队列。它分为独占模式和共享模式&#xff0c;本文主要围绕独占模式进行讲解&#xff0c;共享模式的原理和独占模式相似&#xff0c;最后会提一嘴。 场景代入 其实AQS模…

云计算基础(持续更新)

文章目录 云计算云计算的定义第1关&#xff1a;云计算定义第2关&#xff1a;云计算的基本原理 云计算出现的背景第1关&#xff1a;云计算出现的背景第2关&#xff1a;云计算的特征第3关&#xff1a;云计算的优势与劣势 虚拟化的类型第1关&#xff1a;虚拟化的定义第2关&#xf…

第六章结构型模式—代理模式

文章目录 代理模式解决的问题概念结构 静态代理动态代理织入的概念JDK 动态代理JDK 动态代理分析 CGLIB 动态代理 三种代理的对比JDK 和 CGLIB 的区别动态代理和静态代理的对比代理模式的优缺点使用场景 结构型模式描述如何将类或对象按某种布局组成更大的结构&#xff0c;有以…

浅谈springboot启动过程

1. 知识回顾 为了后文方便&#xff0c;我们先来回顾一下spring的一些核心概念。 spring最核心的功能无非是ioc容器&#xff0c;这个容器里管理着各种bean。ioc容器反映在java类上就是spring的核心类ApplicationContext。ApplicationContext有众多的子接口和子类&#xff0c;不…