首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++笔记-智能指针的使用及其原理

C++笔记-智能指针的使用及其原理

作者头像
海棠蚀omo
发布2026-01-12 17:18:27
发布2026-01-12 17:18:27
1990
举报

1.智能指针的使用场景分析

在我们上一篇讲解异常的时候,这部分的代码我们指出了一种情况,我们来回顾一下:如果array2的new失败了,抛出了异常,就会导致内存泄漏的问题。

而在上一篇中我们也给出了解决方法,也就是array1和array2都放在try/catch语句中,但是这种方法代码重复度高,并且代码也不美观,所以这篇的智能指针就是为了解决这类问题而出现的。

2.RAII和智能指针的值设计思路

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是

⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指

针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,

资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常

释放,避免资源泄漏问题。

能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀

样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。

下面我们来实现一个简单的智能指针:

上图就是我们所实现的智能指针,看起来并不复杂对吧,在这里确实不复杂,复杂的情况我们下面再讲。

上面我们就通过智能指针来控制array1和array2中的资源,并且我在里面重载实现了*/->/[]等运算符,使其使用起来更像一个指针。

而从上面的输出结果过我们可以看出来,不需要try/catch语句来捕捉new的异常,我们依然可以顺利释放array1和array2中的资源,第二张图就是简化过的代码,此时代码看起来就要比上面的简洁和美观许多。

此时array1和array2的生命周期就和智能指针一样,在当前函数的栈帧结束后,就会通过析构函数一起进行释放。

3.C++标准库智能指针的使用

我们都想到这个问题,C++委员会当然也想到了这种问题,所以C++标准库中也引入了智能指针,C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以使⽤了, 智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解决智能指针拷⻉时的思路不同。

auto_ptr 是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给

拷⻉对象,这是⼀个⾮常糟糕的设计,因为它会使被拷⻉对象悬空,访问报错的问题,C++11设计

出新的智能指针后,强烈建议不要使⽤auto_ptr。其他C++11出来之前很多公司也是明令禁⽌使⽤

这个智能指针的。

我们创建一个Date类举例,此时我们对ap1进行了初始化,并且将ap1赋值给ap2,然后我们再通过ap1对其资源进行修改,此时代码看起来没有问题是吧,我们运行一下来看:

此时就会报错,原因上面也说了,ap1赋值给ap2后,这块资源的管理权就发生了变换,ap1就无权在管理这块资源,所以我们再通过ap1来对资源的数据进行修改就会报上面的错误。

我们通过调试来观察ap1和ap2的变化,第一张图是还没有赋值的时候ap1的情况,此时的是数据就是没问题的,而第二张图就是赋值之后ap1和ap2的变化,可以看出底层对于管理权的交换其实就是将进行赋值的智能指针置为空,这样就没有办法再管理资源。

相信大家也能感觉到这种方式设计的并不好,所以并不建议大家使用。

unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,它的特点的不⽀持拷

⻉,只⽀持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。

可以看出,unique_ptr并不支持拷贝,如果拷贝就会报错,并且在这里我要提一点:unique_ptr是不支持通过=号来进行初始化的,其实上面的auto_ptr也不支持,原因就是如果用=来进行初始化的,这就相当于隐式类型转换,会产生临时对象,也就会去调用拷贝构造函数,而unique_ptr是不能拷贝的,所以不支持这种初始化。

我们可以看其最后一条,是被delete修饰的,delete我们在C++11章节就讲过,被其修饰就不能生成函数,默认的也不行。

但是我们上面也说了,虽然不能拷贝,但是可以转移,这里就可以通过将ap3转化为右值来进行转化,原理就和上面的auto_ptr一样,不过这是我们主动交换的,还是和auto_ptr不太一样的。

和上面的一样,第一张图是交换前的ap3,第二张图是交换后的ap3和ap4,可以看出,交换后的结果和auto_ptr如出一辙,ap3也被置为空,管理权发生了交换,交换后ap3同样也不能再对资源的数据进行修改等操作,和上面的ap1一样。

shared_ptr 是C++11设计出来的智能指针,它的名字翻译出来是共享指针,它的特点是⽀持拷⻉,

也⽀持移动。如果需要拷⻉的场景就需要使⽤他了。底层是⽤引⽤计数的⽅式实现的。

shared_ptr相比较前面两种就会好很多,它是支持拷贝的,从上面我们可以看到将sp1赋值给sp2和sp3后,我们依旧可以通过sp1来实现对资源的控制,并且此时sp2和sp3也可以对资源进行控制。

并且shared_ptr也是支持移动的,这点和上面的unique_ptr是一样的:

效果和上面的一样,这里就不过多赘述了。

weak_ptr 是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指

针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr

的⼀个循环引⽤导致内存泄漏的问题。具体细节下⾯我们再细讲。

智能指针我们了解上面的这几种就足够了,下面我们来看一下智能指针中一些常用的接口:

这里我以shared_ptr为例,上面就是C++库中实现的相关接口,可以看出*/->/=等相关的重载库中都是实现的,我们主要来看几个没见过的:

1.use_count

上面在介绍shared_ptr时我们讲了它的底层使用引用计数实现的,而这个接口就是得到此时的引用计数的个数,也就是管理资源的智能指针的个数。

拿上面的例子来看,在sp1对sp2和sp3进行赋值后,通过use_count接口来统计个数我们可以看到,此时的个数变为3。

可能有人对引用计数不太了解,这里我简单说一下:在shared_ptr底层实现中,有一个数据count来记录指向同一块资源的智能指针的个数,每次在析构的时候,都先--count,只要此时--count后不是0,就不析构资源,只有当最后一个指针析构时,--count就等于0,此时再释放资源。

2.reset

reset的作用就是让智能指针指向另外一块空间的资源:

可以看到在sp1给sp2赋值后,此时sp1和sp2管理同一块资源,此时调用use_count也是2,但sp2在调用reset后,我们再次调用use_count,结果就变为1,我们从上图也可以看出此时的sp2已经指向了另外一块资源。

3.unique

unique的作用就是判断此时的这块资源是否只有一个智能指针在管理:

可以看到,在sp2指向新的资源之前,此时调用unique结果为false,而sp2指向另外的资源后,再次调用unique结果就为true。

4.operator bool

shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

此时我们对sp1进行初始化,那么它就会管理一块资源,从输出结果我们也可以看出来。

此时将sp1赋值给sp2,sp1会被置为空,我们再次判断结果就会如上图所示,显示没有资源需要管理。

讲了常用的接口,我们再来思考一个问题:智能指针默认是进行delete释放资源,但是如果不是new出来的资源交给智能指针来管理,那么析构时会怎么样?

答案很明显,析构时程序就会崩溃,那么该解决这种问题呢?

为了解决这种问题,智能指针支持在构造时给一个删除器。 所谓删除器本质就是⼀个可调⽤ 对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器, 在智能指针析构时就会调⽤删除器去释放资源。

说到可调用对象,想必大家应该都有印象,在C++11的章节我们讲了可调用对象都有哪些:函数指针,仿函数,lambda

所以这里的删除器就可以传这三者中的任何一个,我们来看示例:

在讲删除器之前我们先来看这种情况,我们默认的析构使用的是delete,但是此时我们new出来的是一个Date类型的数组,靠delete是不行的,而应该是delete[]才行,所以该解决这种问题呢?

因为new[]比较常用,所以为了解决这种问题库里面就专门写了[]的特化版本,底层就是利用delete[]来释放资源。

下面我们来看删除器如何使用:

unique_ptr和shared_ptr使用删除器的方式不太一样,unique_ptr是类模板参数支持的,而shared_ptr是构造函数参数支持的,注意两者传参方式的不同。这两个我都传的是仿函数,我们再看看另外几种的写法:

这就是我们常见的三种可调用对象的运用方式,lambda示例中的decltype关键字可以返回相应的类型。

最后还有实现其它类型的删除器:

也就是我们上面写的最后一个仿函数,如上图我们要处理文件资源,就可以写一个专门处理文件资源的仿函数等可调用对象来进行处理。这里我还写了lambda的例子,大家可以参照一下。

4.智能指针的原理

上面讲了那么多,我们来尝试来实现一下智能指针,auto_ptr和unique_ptr比较简单,就不演示了,这里主要来实现shared_ptr。

这里我们首先实现了shared_ptr的框架,因为shared_ptr的关键就在于引用计数,所以我们接下来要思考如何定义引用计数的变量。

直接在类中定义一个int类型的变量吗?或者用静态变量?

大家想一下能不能做到我们想要的效果。

答案是不行的,首先如果是第一种,那么每个智能都有一个单独的count,那么就会导致资源无法释放,为什么呢?

因为我们要释放资源的前提是引用计数count减为0,但是如果每个智能指针都有一个count,那么它只会减少一次,但是如果一块资源有新的智能指针来管理,我们是需要将count++的,这是shared_ptr的基本逻辑,所以这种方式不行。

而静态变量如果只有一块资源的话没问题,但是我们不可能只开辟一块资源,如果过开辟了另外的资源,并且如果过第一块资源的有3个智能指针在管理,而第二块资源只有一个智能指针在管理,但是此时是静态变量,所有的资源共用一个count,那么同样也会导致资源无法释放。

那该如何解决这个问题呢?

这里解决问题的关键是我们的需求是一块资源有一个count来计数,不同的资源有不同的count,所以这里解决问题的方法是在第一个智能指针初始化时new一个1,后续如果有新的管理这块资源的智能指针,那就让count++即可。

这样每块资源的智能指针都指向一个count,析构的时候就会按照我们上面讲的count依次--,最后当--count等于0时,就可以析构当前的资源。

我们这样写就可以解决这种问题了,下面我们来实现一下定制删除器的功能:

要实现删除器,我们就要有一个成员变量来接收各种类型的可调用对象,没错,就是我们之前讲的function,它可以接收任意类型的可调用对象,并且因为shared_ptr是构造函数参数支持的删除器,所以只能在类内再写一个构造函数的函数模板来接收不同的可调用对象。

这里我给_del设定了缺省值,和库里面一样,默认就是delete,如果要释放其他类型的资源,就和上面一样传参即可。

注意function中的参数类型,因为我们实现的都是释放资源的的操作,所以不需要返回什么,所以返回值我们直接用void即可,T*想必就不用多解释了,就是我们资源的类型。

解决了上面的删除器,我们再来看拷贝构造函数该如何写:

拷贝构造相对来说比较简单,直接将拷贝对象的各个数据赋值给过去即可,并且不要忘了要把count++,因为此时有两个智能指针指向同一块资源。

写到这里我们就可以自己实验一下,看是否写的正确:

我们简单些,将sp1初始化后,我们打印出此时的count是多少,再将sp1赋值给sp2,再次打印count,从上图我们可以看出,经过拷贝构造过后,count已经变为2,说明sp1指向的资源现在有两个智能指针在管理。

我们通过调试也可以看到,赋值后,sp1和sp2的信息是一样的,也说明我们的拷贝构造函数是成功的。

最后就是最容易出错的=符号重载,我们来看:

为什么所这里很容易出错呢?

很多人以为=符号重载不就和上面的拷贝构造一样嘛,直接把上面的拷贝构造的内容照抄下来,这时候就是出错的关键了,我们设想一下,我们既然用了=符号重载,说明当前的智能指针已经初始化了,已经指向了一块资源,如果我们和上面的拷贝构造函数一样直接赋值,那么原来指向的那块资源怎么办呢?

此时就会发生资源泄露,所以我们上面才要判断这块资源是否只有这一个智能指针在管理,如果有两个及以上,那么就可以直接赋值了,毕竟还有其他的智能指针在管理;而如果只有这一个智能指针在管理,我们是需要释放资源了,避免资源泄露。

外层的判断相信大家应该都能明白,就是判断是否是自己赋值给自己。

再下来我们来实现一下析构函数:

析构函数的逻辑和上面的=符号重载中的一些逻辑一样,其实这个逻辑也是库中shared_ptr的release接口实现的逻辑,这部分的代码我们可以直接写个release函数,=符号重载和析构函数直接调用这个函数即可。

简化后的代码就如上图所示。

5.shared_ptr和weak_ptr

5.1shared_ptr的循环引用问题

shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会

导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使

⽤weak_ptr解决这种问题。

那什么是循环引用问题呢?我们来看下面的示例:

此时的代码看起来没什么问题吧,这就和我们之前讲的循环链表差不多,我们来运行来看一看:

发现问题了没有?

没错,怎么没有调用析构函数来释放资源呢?

这就是循环引用问题所导致的结果,会导致内存泄漏,资源无法被释放,为什么会这样呢?我们通过图来观察:

我们把这个过程用图形来演示,问题就出在最后一点,当n1和n2都被释放后,接下来就该next和prev来释放了,它们两个释放后,资源才能被释放,而这就是问题所在。

要想释放next,就得next当前的资源被释放了,next才会被释放,而next所在的资源要想释放,就得prev被释放。

而prev要想被释放,就得prev所在的资源被释放,而prev所在的资源要想被释放,就得next被释放。

然后就这样陷入了逻辑死循环,双方都得先让对方释放后自己所指向的资源才能被释放,这就是循环引用问题。

那要怎么解决这种问题呢?

我们上面在介绍weak_ptr时就说了它是专门来解决shared_ptr的循环引用问题的,那么它是如何解决的呢?我们来看:

效果就如上图所示,用weak_ptr来指向一块资源,并不会计入引用计数中,上面的next和prev指向相应的资源,可以看出use_count返回的值并没有改变。

所以上面的示例在n1和n2被释放后,引用计数--后变为0,就回去释放相应的资源,也就解决了这种问题。

5.2weak_ptr

weak_ptr 不⽀持RAII,也不⽀持访问资源,所以我们看⽂档时会发现weak_ptr构造时不⽀持绑定到资 源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。

这里就是weak_ptr中的一些接口, weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的 资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤ lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如 果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

weak_ptr中的接口我们只了解expired和lock就足够了:

expired接口的作用就是判断weak_ptr所指向的资源是否被释放,也就是判断此时的weak_ptr智能指针有没有过期。

但是现在我们让sp1和sp2都指向其他的资源,那么weak_ptr所在的资源就会被释放,weak_ptr就过期了,最后就会返回true。

而lock的作用我们上面也说了,它会返回一个管理资源的shared_ptr,相当对多了一个shared_ptr智能指针来管理资源,相应的引用计数也会增加。相反的,如果weak_ptr所在的资源已经被释放了,那么lock返回的shared_ptr就是一个空对象,不能用其管理资源。

最后,关于shared_ptr的问题还没有结束,等讲解到相关的知识点时会再讲。

以上就是智能指针的使用及其原理的内容。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档