智能指针的使⽤及其原理

一. 智能指针的使⽤场景分析

        

        下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到 执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛 出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了。

        

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

        

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

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

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

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

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

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

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

        

        这是我们写的一个类似的模拟智能指针的场景,下面我们来看一下还是需要一个int数组,我们看看只能指针是如何做的。

        

        我们还是传入了一个1,0,发现b为0之后,抛出一个异常,然后找到catch来打印异常,此时我们的Func函数已经结束了此时我们创建的两个sp1和sp2也要跟着销毁,所以该调用析构函数了,析构帮我们释放内存。

三. C++标准库智能指针的使⽤

        但是我们上面的只能指针还是存在缺陷的,我们来看一下。

        

        我们使用了拷贝构造,此时我们指针默认是浅拷贝,我们如果运行就会出问题。

        

        此时程序终止了,就是一块内存被释放两次了,这是这里很不舒服的地方。

        

C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了,

智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解

决智能指针拷⻉时的思路不同。

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

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

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

这个智能指针的。

        这个auto_ptr在讲之前先告诫一下,这个东西一定不要使用,这个东西是很坑的,我们先来看一下它的底层。

        

        大概看一下,下面我们还会模拟实现一下的,它就是把你的原来指向这块地址的那个指针给置空,只让新指针指向这里,这是很坑人的,如果你访问原来的那个指针,它就会空指针引用了。

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

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

        这个就是实现不支持拷贝的

unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
        这是很重要的两行代码,就是把它的拷贝构造给delete一下,就是不让使用的,如果你使用就会报错。

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

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

        这个函数是支持多个指针指向同一块地址的,但是它有一个巧妙的地方就是运用了引用计数,只有你把三个指针都给置空或者指向他处,这块空间没有指针指向,引用计数为0,此时这块空间才释放。

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

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

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

        

四. 智能指针的原理

下⾯我们模拟实现了auto_ptr和unique_ptr的核⼼功能,这两个智能指针的实现⽐较简单,⼤家了

解⼀下原理即可。auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可

的,也不建议使⽤。unique_ptr的思路是不⽀持拷⻉。

        

        可以看一下这个设计思路,它就是把这个拷贝构造的时候,把我们的管理权交给新的指针,自己就置空,不再管理了,这个了解一下就行,基本没人用。

        确实不允许左值的拷贝构造。

        

        这是我们的unique_str,这个就是直接禁用你的拷贝构造了,你传入左值就会报错但是你传入右值就会走右值的拷贝构造,此时这里置空是合理的,因为右值就是我们不要的资源吗。

⼤家重点要看看shared_ptr是如何设计的,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个

引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智

能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计

数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀

个管理资源的对象,则析构资源。

        下面我们看看最重要的shared_ptr吧。

        

        我们先让它能跑起来,其次我们就要解决引用计数这个问题了,直接使用count是不行的,这样你无法使你指向这块地址的每个指针的count保持一致,每个类都有自己的count都是不一样的,这是肯定不行的,那么怎么办呢?

        有人可能想到用静态的变量,这样也是不行的。

        因为这样会使我们全部的类都是用的是一个count,此时不管你是不是指向这块地址的都会使用这个count。

        那么我们怎么办呢?

        我们可以使用指针。

        

        我们这样设计一下,就是使用一下指针,开辟一块空间只存放我们的这个引用计数的值,每次创建一个对象,就开辟一个空间存放它的值,如果你通过拷贝构造创建对象的话,此时就让我的这个引用计数这个指针指向你的引用计数指针的那块地址,然后再++即可。这样它俩指向同一块地址,值自然一样。

        

        看一下这个例子。

        

        我们发现它俩的引用计数的值是一样的,此时就符合我们的预期了。

        下面我们需要搞一下赋值操作,这个相对来说还是比较麻烦一点的。

        

        我们想到赋值,可能就会这样直接写一个赋值,就是把你=右边的_ptr和pcount都给我然后再++即可,这样是错误的,因为你赋值是给一个已经存在的对象赋值的,所以,你需要赋值的这个对象可能就他自己指向一块空间,此时你让它指向别处,此时不就出现内存 泄漏了吗,所以还要判断一下。

        

        我们还需要这样一个操作,看看你是否只是自己指向这块内存的,如果是就释放掉,不是就直接指向下面的,但是这样就万事大吉了吗,我们要是自己给自己赋值呢?比如sp1和sp2指向同一块空间,sp1=sp1,sp1=sp2这都是自己给自己赋值,我们怎么解决呢?

        我们发现自己给自己赋值有一个特点就是我们的_ptr是一样的,所以我们只需要加一个判断即可。

        

        这样就可以了,我们来试一下。

        ​​​

        

        我们发现是符合我们预期的。

        

        如果我们给的是一个自定义类型呢?

        这里我们运行一下。

        

        此时还是存在问题的。

        我们库中的不能释放的原因如下。

std::shared_ptr 不能释放(崩溃)的原因

核心是 「释放方式不匹配」

  • 你用 new[] 分配数组(需 delete[] 释放);
  • std::shared_ptr 默认用 delete 释放资源(仅适配 new 分配的单个对象);
  • delete 无法识别数组内存的元信息,只会析构第一个元素 + 破坏堆结构,导致崩溃。

        而我们用的是delete[]释放内存的,刚好可以。

        我们其中一种解决办法是换一下类型就行了。

        

        这样就可以了,但是还是存在一个问题的,就是我们要是malloc的对象还是不行。

        此时c++库中就给了一个定制删除器的概念,我们来看一下吧。

        

智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指

针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤

对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,

在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new,Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。

  我们先来看看用法吧。

   

unique_ptr和shared_ptr⽀持删除器的⽅式有所不同

unique_ptr是在类模板参数⽀持的,shared_ptr是构造函数参数⽀持的

这⾥没有使⽤相同的⽅式还是挺坑的

使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤

但是下⾯的函数指针和lambda的类型不可以

        这是我们的一个玩法,需要注意的是这两个的写法,一个是在函数模板的时候实例化这个参数,一个是在构造函数中实例化参数,此时就可以完成我们的需求了,我们来看一下。

        

        符合我们的预期。

        

        这是我们使用函数来完成的,看看区别,这里你的unique需要传一个函数指针实例化,并且还需要传入一个函数,否则你的函数指针为空又会出现问题了,相对来说还是我们的shared_ptr用着方便。

        

        这第三个是使用lambda那个实现的,我们的decltype是c++库中提供的一个函数,用来获取lambda类型使用的,总体来说还是我们的shared好用。

        下面我们在我们实现的函数里面实现一下这个操作吧。

        

        我们需要先加一个删除器的构造函数,下面又来了一个问题,我们的这个删除器应该写成什么类型呢,他可能是仿函数,lambda或者函数指针,我们应该定义成什么类型呢,这里就要用到我们之前学的包装器了。

        

        这样修改了一下,我们此时就能完成了,我们换成我们写的来测试一下。

        

        我们发现是没有问题的。

        但是也是存在一个隐藏的问题的,如果你不传第二个参数,此时我们无法删除,我们举个例子吧。

        

        我们的sp5,运行一下。

        

        程序崩溃了,这是怎么回事呢?

        原因就是我们没有传入第二个参数,此时这个_del为空,当然会报错了,我们只需要给一个缺省值即可。

        

        

        此时就不会崩溃了。

template <class T, class... Args> shared_ptr<T> make_shared

(Args&&... args);

shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值

直接构造。

        我们看一下这个东西。

        

        第一个走的就是我们的显示构造,直接构造,完全合法,第二个使用的是我们的make_shared,这个走的是我们的可变参数模板,效率更高,记着就行。

        

        这也是简单的写法。

shared_ptr unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个

空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断

是否为空。

        

        这个你简单理解一下就是把对象赋予了指针的性质,强制给了对象一个bool类型的返回值,此时你就可以像指针一样进行判断。

shared_ptr unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换

成智能指针对象。

        最后两个是错误写法,因为一些老的编译器不支持隐式类型转换或者不会优化,此时代价就大,所以c++11直接给了一个关键字explicit ,此时你就不能通过隐式类型转换来构造。

        像这样就行了。

        

        虽然我们的share_ptr很好用,但是还是存在坑的,下面我们来看一下。

        

五. shared_ptr和weak_ptr

        

5.1 shared_ptr循环引⽤问题

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

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

⽤weak_ptr解决这种问题。

        我们来看一下下面这个场景。

        

        我们看一下这个情况。

        

        我们发现没有释放内存,这必定会导致内存泄漏的,我们下面先来分析明白这个问题。

        

           n1由n1和n2的prev管理的,n2由n2和n1的next管着呢。

        

        n1,n2销毁了,但是next和prev还存在。

        

        这时候就存在了这样的一个问题,此时你无法释放内存了。

  1. n1 和 n2 初始引用计数都是 1(各自管理自己的 ListNode);
  2. n1->_next = n2 → n2 的计数变成 2(被 n1 和自身持有);
  3. n2->_prev = n1 → n1 的计数变成 2(被 n2 和自身持有);
  4. 程序结束时,n1 和 n2 析构:
    • n1 析构 → n1 的计数从 2→1(还被 n2->_prev 持有);
    • n2 析构 → n2 的计数从 2→1(还被 n1->_next 持有);
  5. 最终两个 ListNode 的计数都是 1,永远不会减到 0,资源无法释放,造成内存泄漏。

        此时我们就要用到我们的weak_ptr了,它的作用就是不增加我们的引用计数,我们来试一下。

        

        

        此时就没有问题了,析构了。

        

5.2 weak_ptr

        

weak_ptr不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weak_ptr构造时不⽀持绑定到资

源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_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,但是库中远比这个复杂的多,我们只是帮助我们理解一下。

        我们的weak_ptr没有实现*和->操作符,因为我们的weak_ptr很容易指向空,很容易空指针引用,很麻烦。

        

        再来介绍几个函数,第一个就是判断是否过期的。

        这个use_count就是获取引用计数的。

        

        返回0就是没有过期,1就是过期了。

        

        返回1就是过期了。

        那么我想访问资源怎么办呢?

        就要用到另外一个函数了。

        ​​​​​​​

        就是我们这里的lock函数了,这个函数的作用就是返回一个shared对象和你一起共同管理。

        

        从这个结果中也可以看出来,我们的count++了。

        

六. shared_ptr的线程安全问题

        

shared_ptr的引⽤计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷

⻉析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者

原⼦操作保证线程安全的。

shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr

管,它也管不了,应该有外层使⽤shared_ptr的⼈进⾏线程安全的控制。

下⾯的程序会崩溃或者A资源没释放,bit::shared_ptr引⽤计数从int*改成atomic<int>*就可以保证

引⽤计数的线程安全问题,或者使⽤互斥锁加锁也可以。

        这个了解一下就行了。

七. C++11和boost中智能指针的关系

        

Boost库是为C++语⾔标准库提供扩展的⼀些C++程序库的总称,Boost社区建⽴的初衷之⼀就是为

C++的标准化⼯作提供可供参考的实现,Boost社区的发起⼈Dawes本⼈就是C++标准委员会的成员

之⼀。在Boost库的开发中,Boost社区也在这个⽅向上取得了丰硕的成果,C++11及之后的新语法

和库有很多都是从Boost中来的。

C++ 98 中产⽣了第⼀个智能指针auto_ptr。

C++ boost给出了更实⽤的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等.

C++ TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版。

C++ 11,引⼊了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的

scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

        了解着看一下。

        

八. 内存泄漏

        8.1 什么是内存泄漏,内存泄漏的危害

        

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释 放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分 配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。 内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射 关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服 务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越 慢,最终卡死。

        

8.2 如何避免内存泄漏

        

⼯程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理

想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理

才有保证。

尽量使⽤智能指针来管理资源,如果⾃⼰场景⽐较特殊,采⽤RAII思想⾃⼰造个轮⼦管理。

定期使⽤内存泄漏⼯具检测,尤其是每次项⽬快上线前,不过有些⼯具不够靠谱,或者是收费。

总结⼀下:内存泄漏⾮常常⻅,解决⽅案分为两种:1、事前预防型。如智能指针等。2、事后查错

型。如泄漏检测⼯具。

九.结束语

         感谢读到这里的每一位朋友!技术之路漫长,每一次代码的调试、每一个知识点的梳理,都因你的驻足而更有意义。如果文章对你有帮助,欢迎点赞收藏,也期待在评论区和你交流更多技术细节~本期的技术分享就到这里啦!感谢你的耐心观看。文中若有疏漏或更好的优化方案,欢迎随时指出,一起在技术的世界里共同进步!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值