一、引入
首先通过一个使用场景来引入智能指针。这里有一个类 HF,一个子函数 fun,在 fun 里面 new 了三个 HF 对象,然后 delete。正常情况下 delete 会先调用析构函数,然后再释放相应的资源。
二、智能指针的两大特性
- RAII
- 行为像指针
1、RAII
是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。(通俗来讲,就是将资源交给一个类对象来管理,通过该类的构造函数交给对象。)
特点
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
好处
(1)、不需要显式地释放资源,而是通过智能指针间接帮忙释放;(2)、采用这种方式,对象所需的资源在其生命期内始终保持有效。
2、行为像指针
智能指针实际也是一个类,要是行为像一个指针,即要重载解引用(*),箭头(->),甚至有时还要重载方括号([])。
三、智能指针起初的缺陷:拷贝问题
首先我们实现一个简易版智能指针:new 了一个日期类对象交给智能指针管理,智能指针对象存在期间,资源都是存在的,最后智能指针对象生命周期结束,调用析构函数释放,同时释放掉资源(delete);
但是当我们用对象 sp1 去拷贝构造 sp2 时,此时就会报错。原因在于我们没有实现拷贝构造,此时默认拷贝构造就是浅拷贝,这样两个对象的成员变量都会指向这份资源,最后调用析构函数时,就会对这份资源 delete 两次,从而造成野指针的问题。如何解决这个问题,在第四点进行介绍。
四、几种智能指针的介绍
1、C++98 出现的智能指针——auto_ptr
头文件:memory。具体信息可以查看官网文档。
auto_ptr 解决上述拷贝构造的问题
auto_ptr 是直接将资源的管理权转移,用对象 sp1 去拷贝构造 sp2,那么就会将 sp1 的资源的管理权交给 sp2 管理,而 sp1 被置空。
大致处理方法如下:拷贝后将 sp1 置空就行了。
注意,C++11 的移动语义也是资源的转移,但和这里是不一样的,移动语义是针对将亡值去转移资源,而这里 sp1 不是将亡值。
这样做是有些问题的,这里资源转移后,sp1 就悬空了,此时拷贝后就不能去访问 sp1,否则就会出现空指针的问题,所以很多公司都禁止使用 auto_ptr。
2、boost 库
提到智能指针,就得提一下 boost 库,boost 库是 C++ 第三方库,里面就有智能指针,而 C++ 的智能指针就是从这个库里面引入的,然后进行了略微修改。
3、unique_ptr
该智能指针解决拷贝构造的问题的方法就是:简单粗暴,禁止拷贝,适用于不需要拷贝的场景。
底层实际就是将拷贝构造给 delete 了。同时,赋值运算符重载也要禁掉,默认生成的赋值运算符重载也是浅拷贝。
4、shared_ptr
当遇到需要拷贝构造的场景时,就需要使用 shared_ptr,shared_ptr 解决拷贝构造的问题的方法是:引用计数,去解决多次析构的问题。
引用计数的实现
引用计数:记录当前有几个对象参与管理这个资源,在某个对象析构时,就将引用计数减 1,当最后一个对象析构时才去释放资源。
要实现引用计数,就需要一份资源对应一个计数,有人会想到定义一个静态成员 count,但实则不行,因为静态成员是属于整个类的,属于所有对象。管理一个资源的时候是可以解决的,但当第二个资源出现时,就不能适用了,因为不同资源之间的引用计数都是同一个静态成员变量,所以会相互影响。
实际上的实现如下:增加一个成员变量*pcount,即指向引用计数的指针,在构造的时候(即资源来了),就 new 一个计数给该指针,在拷贝构造发生的时候,除了使两个对象指向同一个资源外,两个对象的引用计数也要指向同一个,并且要记得把引用计数++一下,在某个对象析构时,就将引用计数减 1,然后判断是否为最后一个对象的析构,如果是的话就释放资源。
赋值运算符的问题:(循环引用)
shared_ptr 虽然解决了拷贝构造的问题,但正因为引用计数的这样实现,又会造成赋值运算符重载后出现问题。
为了分析这里的缺陷,我们引入一个场景:双向链表的赋值。
这是双向链表的简单实现。因为会将链表资源交给智能指针管理,所以链表的定义中,成员 next 和 prev 的类型也应该是智能指针,不然在赋值的时候会出现类型不同的问题,正因为需要这样设计,问题就来了。


