终于来填坑了😂
1 2022-10-19 -> 2023-04-01 
1.情景 
对于C/C++而言,内存泄漏是一个老生常谈的问题。每次进行new操作之后,我们都需要对其进行对应的delete,已避免内存泄漏。
可代码一长,逻辑复杂起来了,想处理就没有那么容易了。
1.1 代码太长,看不到头 
1 2 3 4 5 6 7 8 int  main ()     int * arr1 = new  int [10 ];     int * arr2 = new  int [10 ];          delete  arr1[];     delete  arr2[]; } 
我们日常的学习,可能写的代码并不是很多。但如果是一个大型的项目,new和delete之中可能隔了成千上万 行代码,那时候还想去找这个变量的位置,可就不那么好找了。极其容易出现忘记delete的情况
1.2 异常安全 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int  func (int  a,int  b)     if (b==0 )     {         throw  invalid_argument ("除0错误" );     }     return  a/b; } void  test ()     int * arr1 = new  int [10 ];     int * arr2 = new  int [10 ];     func (1 ,0 );     delete [] arr1;     delete [] arr2; } int  main ()     try {          test ();     }        catch (...){         cout <<"出现异常" <<endl;     }     return  0 ; } 
如上面的代码,在test函数中,调用了另外一个会抛出异常的函数。如果这个函数抛出了异常,后续的delete操作不会被执行,出现内存泄漏。
如果操作的堆区变量较少,可以采用如下的方式来解决这个问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 int  func (int  a,int  b)     if (b==0 )     {         throw  invalid_argument ("除0错误" );     }     return  a/b; } void  test ()     int * arr1 = new  int [10 ];     int * arr2 = new  int [10 ];     try {         func (1 ,0 );     }     catch (...){                  delete [] arr1;         delete [] arr2;         throw ;     }     delete [] arr1;     delete [] arr2; } int  main ()     try {          test ();     }        catch (...){         cout <<"出现异常" <<endl;     }     return  0 ; } 
但是,这样并不能完全解决问题,因为new函数本身也有可能抛出异常!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void  test ()     int * arr1 = new  int [10 ];     int * arr2 = new  int [10 ];                    try {         func (1 ,0 );     }     catch (...){                  delete [] arr1;         delete [] arr2;         throw ;     }     delete [] arr1;     delete [] arr2; } 
把new也丢到try里面?依旧不行(详见注释)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void  test ()     try {         int * arr1 = new  int [10 ];         int * arr2 = new  int [10 ];         int * arr3 = new  int [10 ];              func (1 ,0 );     }     catch (...){                                    delete [] arr1;         delete [] arr2;         delete [] arr3;         throw ;     }     delete [] arr1;     delete [] arr2;     delete [] arr3; } 
而且,如果需要操作的new变量很多,那么在catch里面就需要加上多个delete,代码就显得过于重复了。
为了解决这个问题,C++引入了智能指针 
2.智能指针 
2.1 RAII 
1 RAII - Resource Acquisition is Initialization 
需要注意的是,RAII是一种思想 ,并不能用它来指代智能指针
它是一种利用对象的生命周期来控制程序资源(内存、文件句柄、网络链接、互斥量等)的技术
只要是两步操作的,需要申请+释放的资源,都可以使用RAII的思想来进行处理。比如自己封装一个自动处理pthread_mutex锁的init和destory的类,std::unique_lock就是使用了RAII思想的锁管理类,会自动加锁并在出作用域的时候解锁。
 
说人话就是,在对象构造 的时候获取资源,析构 的时候释放资源。
在对象生命周期到了之后,会自动释放资源,免去了我们手动释放资源or忘记释放的繁琐; 
资源在对象的生命周期内始终有效; 
 
在大型项目中,一般都会用智能指针来管理堆区空间。
3.demo 
3.1 基础示例 
在认识不同类型的智能指针之前,先来看个最简单的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #pragma  once namespace  mu {    using  std::cout;     using  std::endl;     template <class  T >     class  SmartPtr      {     public :         SmartPtr (T* ptr)             :_ptr(ptr)         {             cout << "init "  << (void *)_ptr << endl;         }         ~SmartPtr ()         {             cout << "des  "  << (void *)_ptr << endl;             delete [] _ptr;         }     private :         T* _ptr;     }; } 
上面实现的,就是一个最简单的智能指针。它可以帮助我们管理堆区上的数组 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define  _CRT_SECURE_NO_WARNINGS 1 #include  <iostream>  #include  "demo.hpp"  using  namespace  std;void  test1 ()     mu::SmartPtr<int > p1 (new  int [10 ])  ;     mu::SmartPtr<char > p4 (new  char [10 ])  ;     mu::SmartPtr<double > p3 (new  double [10 ])  ;     cout << "test"  << endl; } int  main ()     test1 ();     return  0 ; } 
运行后,输出的结果如下
1 2 3 4 5 6 7 init 006697A8 init 006724D0 init 00668860 test des  00668860 des  006724D0 des  006697A8 
其实现了在构造中托管,在析构中销毁资源的操作。
即便抛出异常 ,依旧能正常析构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include  <iostream>  #include  "demo.hpp"  using  namespace  std;int  func (int  a, int  b)     if  (b == 0 )     {         throw  invalid_argument ("除0错误" );     }     return  a / b; } void  test1 ()     mu::SmartPtr<int > p1 (new  int [10 ])  ;     mu::SmartPtr<char > p2 (new  char [10 ])  ;     mu::SmartPtr<double > p3 (new  double [10 ])  ;     cout << "test1"  << endl;     try  {         func (3 , 0 );     }     catch (...){         throw ;     }          cout << "test2"  << endl; } int  main ()     try  {         test1 ();     }     catch  (...)     {         cout << "mian  出现异常"  << endl;     }     return  0 ; } 
注意,如果抛出异常又不进行catch,程序会被abort终止,无法观测到现象。
以下是运行的结果,可以看到异常出现后,走到了test1函数的生命周期末尾,释放了3个指针,才被main中的catch捕获
1 2 3 4 5 6 7 8 init 012B8F00 init 012C3010 init 012B8860 test1 des  012B8860 des  012C3010 des  012B8F00 mian  出现异常 
3.2 运算符重载 
当然,这个智能指针还是却少很多东西的
写死了delete[],我只想让她管理单个变量怎么办? 
如何获取指针内的资源? 
 
第一个问题我们暂且不提(后续讲解库中智能指针的时候会说明)第二个问题的答案便是:重载*和->操作符
1 2 3 4 5 6 7 8 9 10 11 12 T& operator *() {     return  *_ptr; } T* operator ->() {     return  _ptr; } T& operator [](const  T& n) {     return  _ptr[n]; } 
重载了之后,我们就可以操作类内的指针成员了
1 2 3 4 5 6 7 8 9 10 11 12 void  test1 ()     mu::SmartPtr<int > p1 (new  int [10 ])  ;     mu::SmartPtr<char > p2 (new  char [10 ])  ;     mu::SmartPtr<double > p3 (new  double [10 ])  ;     cout << "test"  << endl;     p2[0 ] = 'a' ;     p2[1 ] = 'b' ;     p2[2 ] = '\0' ;     cout << p2[0 ] <<" " << p2[1 ] << endl;     cout << *p2 + 1  << endl; } 
运行结果如下
1 2 3 4 5 6 7 8 9 init 01661C30 init 01671748 init 016689B8 test a b 98 des  016689B8 des  01671748 des  01661C30 
3.3 拷贝 
对于智能指针而言,有一个很重要的问题是针对拷贝的。在一些场景中,我们需要对指针进行拷贝,这时候就会出现异常
1 2 3 4 5 void  test3 ()     mu::SmartPtr<int > p1 (new  int [10 ])  ;     mu::SmartPtr<int > p2 (p1)  ; } 
运行结果如下
1 2 3 init 00BBFB20 des  00BBFB20 des  00BBFB20 
编译器报错了
原因就是,默认的拷贝构造使用的是浅拷贝,再析构的时候,两个智能指针对同一个地址析构,相当于析构了两次,肯定是不行的!
接下来,就让我们看看cpp库中是怎么解决这个问题的吧!
4.auto_ptr 
为了避免拷贝的时候,导致多次析构,C++98库函数中提供了auto_ptr。在拷贝的时候,auto_ptr采用的是管理权转移 的思路
1 2 auto_ptr<int >  sp1 (new  int )  ;auto_ptr<int >  sp2 = sp1; 
当我们这样操作了之后,sp1对象将不能再被使用,其内置指针会被置为nullptr,使用相当于解引用空指针!
1 2 3 4 auto_ptr<int > sp1 (new  int (10 ))  ; auto_ptr<int > sp2 = sp1;     cout << *sp2 << endl;  cout << *sp1 << endl;  
因为这个操作实在太坑人了,如果在某些函数传参的时候,进行值拷贝了,就会导致原有的指针失效,从而引发程序错误。
所以,在C++11之后,应避免使用 auto_ptr。在使用新版本g++进行编译的时候,也会提示auto_ptr已经被抛弃了。
1 warning: ‘template<class> class std::auto_ptr’ is deprecated: use 'std::unique_ptr' instead [-Wdeprecated-declarations] 
5.unique_ptr 
5.1 基本说明 
在boost库中,有一个scpoed_ptr,其就是unique_ptr的前身。
 
unique的做法更绝,既然拷贝会出现问题,那我直接不允许你拷贝 不就行了?
直接将拷贝构造和赋值重载给delete了,即禁止对方使用拷贝。
1 2 unique_ptr (const  unique_ptr<T>& n) = delete ;unique_ptr<T>& operator =(const  unique_ptr<T>& n) = delete ; 
如果是C++11之前,可以采用只声明不实现 的方式来禁用拷贝构造。为了避免使用者自己写一个拷贝构造,我们需要将其配置为私有。
1 2 3 private :  unique_ptr (const  unique_ptr<T>& n);   unique_ptr<T>& operator =(const  unique_ptr<T>& n); 
如果我们写出这样的代码,触发了拷贝构造的场景,此时因为无法访问unique_ptr的拷贝构造,是无法编译成功的。
1 2 unique_ptr<int > sp1 (new  int (10 ))  ;unique_ptr<int > sp2 (sp1)  ; 
虽然unique_ptr从源头解决了拷贝的问题,但是它有一个小问题:功能不全 。如果我真的需要拷贝呢?你这个岂不是用不了呀。
5.2 可以被move吗? 
面试的时候被考到了这个问题,已知unique_ptr无法拷贝,那请问他可以被std::move给其他变量吗?
使用下面的代码进行测试
1 2 3 4 5 6 7 8 9 10 11 unique_ptr<int > sp1 (new  int (20 ))  ;cout << "sp1:"  << *sp1 << endl;  unique_ptr<int > sp2 = std::move (sp1); cout << "sp2:"  << *sp2 << endl;  unique_ptr<int > sp3 (std::move(sp2))  ;cout << "sp3:"  << *sp3 << endl;  cout << "sp1(after move):"  << *sp1 << endl;  cout << "sp2(after move):"  << *sp2 << endl;  
输出如下,访问sp1的时候会段错误(访问sp2的时候也会)
1 2 3 4 sp1:20 sp2:20 sp3:20 [1]    6693 segmentation fault  ./test1 
再来试试赋值重载可以不,会发现是可行的。
1 2 3 4 5 unique_ptr<int > sp3 (new  int (30 ))  ;cout << "sp3:"  << *sp3 << endl;  sp3 = std::move (sp2); cout << "sp3:"  << *sp3 << endl;  
可得结论:unique_ptr可以被move,move后原有对象失效,访问原有对象会段错误。
将上面的unique_ptr换成shared_ptr也是一样的结果,符合右值拷贝的特性。
 
其实这个问题是取决于unique_ptr是否实现了右值拷贝。使用vscode跳转到linux下unique_ptr的源码,可以看到它是实现了右值拷贝的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17      unique_ptr (unique_ptr&&) = default ;            template <typename  _Up, typename  _Ep, typename  = _Require<               __safe_conversion_up<_Up, _Ep>,        typename  conditional<is_reference<_Dp>::value,                 is_same<_Ep, _Dp>,                 is_convertible<_Ep, _Dp>>::type>> unique_ptr (unique_ptr<_Up, _Ep>&& __u) noexcept : _M_t(__u.release (), std::forward<_Ep>(__u.get_deleter ())) { } 
同时也能找到右值的赋值重载。
1 2 3 4 5 6 7 8 unique_ptr& operator =(unique_ptr&&) = default ;
6.shared_ptr 
share即分享,这个智能指针是支持拷贝的。那它应该如何解决同一片空间被释放多次的问题呢?
6.1 引用计数 
为了保证资源只会被释放一次,其采用了引用计数的方式来实现。
初始化的时候,引用计数为1; 
每次拷贝,引用计数都+1(包括拷贝构造和赋值重载); 
析构的时候,引用计数不为1,将计数器-1; 
只有引用计数为1(当前是最后一个对象了),才在析构的时候释放资源; 
 
这样就解决了被析构多次的问题!
6.2 如何实现? 
不行,一个对象的修改不影响第二个对象的成员,依旧会出现析构多次的问题。
static成员属于整个类,这样弄相当于给这个类加了个已有对象数量的计数器,南辕北辙。
我们只需要在对象中添加一个int类型的指针,在第一次初始化对象的时候,给这个指针创建堆区的int空间,并初始化为1;
这样每次拷贝、赋值的时候,都给这个pcount指向的int给+1
每次析构的时候都-1,只有为0的时候,才进行资源释放。同时释放指针托管的内存以及pcount占用的内存。
1 2 3 4 5 6 (*_pcount)--; if ((*_pcount==0 )){     delete  _ptr;     delete  _pcount; } 
6.3 赋值 
赋值有两种情况
1 2 3 4 5 6 shared_ptr<int > sp1 (new  int (1 ))  ;shared_ptr<int > sp2;  sp2 = sp1; shared_ptr<int > sp3 (new  int (3 ))  ;sp1 = sp3; shared_ptr<int > sp4 = sp1; 
第一种  是对象没有初始化,调用了默认无参构造函数,其内部托管的指针是nullptr,sp2=sp1,相当于是初始化sp2对象。
第二种  是对象已经初始化了,但是我想让他管理另外一份资源。
针对情况1,操作和拷贝构造相同,我们只需要赋值给sp2后,将引用计数加+1即可 
针对情况2,我们就需要判断sp1的情况了。如果sp1的引用计数为1,则需要先销毁sp1托管的空间 ,再进行赋值。并将sp3的引用计数赋值给sp1,并将sp3的引用计数+1 
 
落到代码上,应该如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 shared_ptr<T>& operator =(const  shared_ptr<T>& p) {     if (p._ptr == _ptr)     {         return  *this ;     }     (*_pcount)--;     if ((*_pcount==0 ))     {         delete  _ptr;         delete  _pcount;     }          _ptr = p._ptr;     _pcount = p._pcount;     (*_pcount)++;          return  *this ; } 
6.4 简单实现 
库中的实现更为复杂,其还重载了<<操作符,实现了更多成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 template <class  T >   class  shared_ptr     {        public :            shared_ptr (T* ptr = nullptr )                :_ptr(ptr),            _pcount(new  int (1 ))            {                cout << "[init] "  << (void *)_ptr << endl;            }                        shared_ptr (const  shared_ptr<T>& p)            {                cout << "[copy] "  << (void *)_ptr << endl;                _ptr = p._ptr;                _pcount = p._pcount;                (*_pcount)++;            }                        shared_ptr<T>& operator =(const  shared_ptr<T>& p)            {                cout << "[operator=] "  << (void *)_ptr << endl;                if  (p._ptr == _ptr)                {                    return  *this ;                }                                _Release();                                _ptr = p._ptr;                _pcount = p._pcount;                (*_pcount)++;                return  *this ;            }            T& operator *()            {                return  *_ptr;            }            T* operator ->()            {                return  _ptr;            }            T& operator [](const  T& n)            {                return  _ptr[n];            }            T* get ()               {               return  _ptr;            }            ~shared_ptr ()            {                _Release();            }        private :            void  _Release()            {                (*_pcount)--;                cout << "[des] pcount:"  << (*_pcount) << " ptr:"  << (void *)_ptr << endl;                if  ((*_pcount) == 0 )                {                    cout << "[des] delete "  << (void *)_ptr << endl;                     if (_ptr)                        delete  _ptr;                                        delete  _pcount;                }            }        T* _ptr;        int * _pcount;    }; 
6.5 测试 
用如下代码进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void  test4 ()     mu::shared_ptr<int > p1 (new  int (10 ))  ;     mu::shared_ptr<int > p2 (p1)  ;     mu::shared_ptr<int > p3 = p1;     cout << "p1 "  << (*p1) << endl;     cout << "p2 "  << (*p2) << endl;     cout << "p3 "  << (*p3) << endl;     mu::shared_ptr<int > p4;     p4 = p1;     cout << "p4 "  << (*p4) << endl;     mu::shared_ptr<int > p5 (new  int (20 ))  ;     p1 = p5;     cout << "p1 "  << (*p1) << endl;     cout << "p5 "  << (*p5) << endl; } 
运行的结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [init] 00BE0B10 [copy] 00000000 [copy] 00000000 p1 10 p2 10 p3 10 [init] 00000000 [operator=] 00000000 [des] pcount:0 ptr:00000000 [des] delete 00000000 p4 10 [init] 00BD8B30 [operator=] 00BE0B10 [des] pcount:3 ptr:00BE0B10 p1 20 p5 20 [des] pcount:1 ptr:00BD8B30 [des] pcount:2 ptr:00BE0B10 [des] pcount:1 ptr:00BE0B10 [des] pcount:0 ptr:00BE0B10 [des] delete 00BE0B10 [des] pcount:0 ptr:00BD8B30 [des] delete 00BD8B30 
可以看到,每次析构,实际上都会先对引用计数进行-1的操作,只有引用计数为0的情况下,才会真的析构掉对应的值。
赋值的时候,也没有出现内存泄漏的问题!
1 2 3 4 5 6 7 8 9 10 void  test5 ()     mu::shared_ptr<int > p1 (new  int (10 ))  ;     mu::shared_ptr<int > p2 (new  int (20 ))  ;     cout << "p1 "  << (*p1) << endl;     cout << "p2 "  << (*p2) << endl;     p1 = p2;     cout << "p1 "  << (*p1) << endl;     cout << "p2 "  << (*p2) << endl; } 
p2在赋值给p1之前,先析构了p1维护的变元,才进行了赋值操作
1 2 3 4 5 6 7 8 9 10 11 12 [init] 014DF1B8 [init] 014D89A8 p1 10 p2 20 [operator=] 014DF1B8 [des] pcount:0 ptr:014DF1B8 [des] delete 014DF1B8 p1 20 p2 20 [des] pcount:1 ptr:014D89A8 [des] pcount:0 ptr:014D89A8 [des] delete 014D89A8 
6.6 线程安全问题 
在实际应用场景中,可能会出现多线程对该指针进行拷贝的问题。为了避免引用计数pcount在多线程拷贝的时候出现二义性问题,需要对引用计数的操作进行加锁
详见 https://blog.musnow.top/posts/1249427441/  的 12.shared_ptr;释放资源的时候需要使用锁来加锁解锁,保证计数器的操作是原子性的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void  Release ()     bool  flag = false ;     _pMutex->lock ();     if  (--(*_pRefCount) == 0  && _ptr)     {                         delete  _ptr;         delete  _pRefCount;                  flag = true ;     }     _pMutex->unlock ();          if  (flag){         delete  _pMutex;            } } 
每次拷贝的时候,引用计数的操作也需要加锁
1 2 3 4 5 6 7 8 void  AddRef ()     _pMutex->lock ();     ++(*_pRefCount);     _pMutex->unlock (); } 
6.7 循环引用问题 
shared_ptr的引用计数可能会出现循环引用 的问题,它需要用weak_ptr来解决。后文会提到。
6.8 make_shared和直接构造shared_ptr的区别 
来自GPT
 
make_shared 和直接使用 shared_ptr 的构造函数有几个关键区别:
性能:make_shared 在创建对象时将分配内存来同时存储对象和控制块(control block),而直接使用 shared_ptr 的构造函数则会分别分配内存来存储对象和控制块。因此,make_shared 可以减少内存分配的次数,提高性能,尤其在频繁创建和销毁 shared_ptr 对象时更为明显。 
内存管理:make_shared 会将对象和控制块一起存储在一块连续的内存中,这样可以减少内存碎片化。而直接使用 shared_ptr 的构造函数则会分别分配两块内存,可能导致内存碎片化的问题。 
异常安全性:使用 make_shared 可以提供更强的异常安全性,因为对象和控制块是在同一次内存分配中创建的。如果在创建对象或控制块时抛出异常,make_shared 会自动销毁已经分配的内存,避免内存泄漏。而直接使用 shared_ptr 的构造函数可能会导致部分内存泄漏,因为对象或控制块可能已经被成功分配了,但另一块内存分配失败时可能无法正确释放。 
使用方便性:make_shared 的语法更为简洁明了,只需提供对象类型和构造函数参数即可,不需要显式指定 shared_ptr 的模板类型。而直接使用 shared_ptr 的构造函数则需要显式指定模板类型,并且需要分别为对象和控制块进行内存分配和初始化。 
 
综上所述,尽量优先使用 make_shared 来创建 shared_ptr 对象,以提高性能和内存管理效率,并增强异常安全性。
7.weak_ptr 
7.1 简介 
这个指针是专门用来辅助解决shared_ptr循环引用问题的,可以认为它是shared_ptr的小弟。
1 2 3 4 5 6 7 8 9 10 11 12 13 constexpr  weak_ptr ()  noexcept weak_ptr  (const  weak_ptr& x) noexcept ;template  <class  U > weak_ptr  (const  weak_ptr<U>& x) noexcept ;template  <class  U > weak_ptr  (const  shared_ptr<U>& x) noexcept ;weak_ptr& operator = (const  weak_ptr& x) noexcept ; template  <class  U > weak_ptr& operator = (const  weak_ptr<U>& x) noexcept ;template  <class  U > weak_ptr& operator = (const  shared_ptr<U>& x) noexcept ;
相比于其他智能指针的构造函数,weak_ptr只能进行拷贝,或从一个shared_ptr来构造。
它最大的特点就是:只托管资源,不处理引用计数,析构时也不进行资源释放。可以认为,它只是对指针进行了简单的封装。
这个特性也决定了,weak_ptr不能使用原生指针来构造!
7.2 什么是循环引用 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct  ListNode {     ListNode* _prev;     ListNode* _next;     T _val;     ListNode (const  T& val=T ())         :_prev(nullptr ),         _next(nullptr ),         _val(val)     {         cout << "[ListNode()] "  << (void *)this  << endl;     }     ~ListNode ()     {         cout << "[~ListNode()] "  << (void *)this  << endl;     } }; void  test6 ()     mu::shared_ptr<ListNode<int >> p1 (new  ListNode <int >(10 ));     mu::shared_ptr<ListNode<int >> p2 (new  ListNode <int >(20 ));	 } 
上面这个代码是一个最简单的双链表,在没有给链表内节点赋值之前,它是没有问题的。使用智能指针能成功调用对象的析构,并销毁空间
1 2 3 4 5 6 7 8 9 10 [ListNode()] 00DE10E0 [init] 00DE10E0 [ListNode()] 00DE14D0 [init] 00DE14D0 [des] pcount:0 ptr:00DE14D0 [des] delete 00DE14D0 [~ListNode()] 00DE14D0 [des] pcount:0 ptr:00DE10E0 [des] delete 00DE10E0 [~ListNode()] 00DE10E0 
但如果想要将这两个节点链接起来,那就出bug了
首先,自然是我们没办法将一个智能指针赋值给普通的指针,因为类型不同。我们也不能直接对它们进行强转
解决这个问题,那就是将listnode中的指针成员也改成智能指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 template <class  T >struct  ListNode {     mu::shared_ptr<ListNode<T>> _prev;     mu::shared_ptr<ListNode<T>> _next;     T _val;     ListNode (const  T& val=T ())         :_prev(nullptr ),         _next(nullptr ),         _val(val)     {         cout << "[ListNode()] "  << (void *)this  << endl;     }     ~ListNode ()     {         cout << "[~ListNode()] "  << (void *)this  << endl;     } }; void  test6 ()     mu::shared_ptr<ListNode<int >> p1 (new  ListNode <int >(10 ));     mu::shared_ptr<ListNode<int >> p2 (new  ListNode <int >(20 ));     p1->_next = p2;     p2->_prev = p1; } 
再次运行,诶,出问题了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [init] 00000000 [init] 00000000 [ListNode()] 00FFF1B8 [init] 00FFF1B8 [init] 00000000 [init] 00000000 [ListNode()] 00FF8D78 [init] 00FF8D78 [operator=] 00000000 [des] pcount:0 ptr:00000000 [operator=] 00000000 [des] pcount:0 ptr:00000000 [des] pcount:1 ptr:00FF8D78 [des] pcount:1 ptr:00FFF1B8 
可以看到,虽然shared_ptr的析构函数被调用了,但直到最后,都没有打印出[des] delete,也没有进入ListNode的析构函数,即出现了内存泄漏!
1 2 3 4 5 6 7 8 9 10 11 12 13 void  _Release(){     (*_pcount)--;     cout << "[des] pcount:"  << (*_pcount) << " ptr:"  << (void *)_ptr << endl;     if  ((*_pcount) == 0 )     {         cout << "[des] delete "  << (void *)_ptr << endl;          if (_ptr)             delete  _ptr;                  delete  _pcount;     } } 
画个图,看看到底是为甚
p1管理资源A,p2管理资源B;二者引用计数都为1 
p1->next=p2,p1->next也开始管理资源B,引用计数为2 
p2->prev=p1,p2->prev也开始管理资源A,引用计数为2 
出作用域,先析构p2,B引用计数-1,此时资源B是只由p1->next管理的 
后析构p1,A引用计数-1,此时资源A是只由p2->prev管理 
但是p1->next必须要完全析构资源A才会被释放;同理,p2->prev也需要完全析构资源B才会释放 
 
这时候就陷入了一个死循环,因为资源A和资源B实际上已经没有外人 在使用了,它们也无法被彻底释放掉,内存泄漏了!
7.3 解决循环引用问题 
讲到这里,如何解决这个问题,就很明了了。因为weak_ptr是不进行引用计数的操作的,其只对资源进行托管。我们只需要将listnode之中的指针从shared_ptr改为weak_ptr即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 template <class  T >struct  ListNode {          std::weak_ptr<ListNode<T>> _next;     std::weak_ptr<ListNode<T>> _prev;     T _val;     ListNode (const  T& val=T ())         :_val(val)     {         cout << "[ListNode()] "  << (void *)this  << endl;     }     ~ListNode ()     {         cout << "[~ListNode()] "  << (void *)this  << endl;     } }; void  test6 ()     std::shared_ptr<ListNode<int >> p1 (new  ListNode <int >(10 ));     std::shared_ptr<ListNode<int >> p2 (new  ListNode <int >(20 ));     p1->_next = p2;     p2->_prev = p1; } 
可以看到,这时候就能成功释放节点了!
1 2 3 4 [ListNode()] 0117F348 [ListNode()] 01171950 [~ListNode()] 01171950 [~ListNode()] 0117F348 
7.4 简单实现 
库里面的实现比我们这个复杂很多,实现只是为了理解设计思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 template <class  T >class  weak_ptr { public :    weak_ptr ()         :_ptr(nullptr )     {}          weak_ptr (const  shared_ptr<T>& p)         :_ptr(p.get ())     {         _ptr;     }          weak_ptr (const  weak_ptr<T>& p)         :_ptr(p._ptr)     {}          weak_ptr<T>& operator =(shared_ptr<T>& p)     {         _ptr = p.get ();         return  *this ;     }     weak_ptr<T>& operator =(const  weak_ptr<T>& p)     {         _ptr = p._ptr;         return  *this ;     }     T& operator *()     {         return  *_ptr;     }     T* operator ->()     {         return  _ptr;     }     T& operator [](const  T& n)     {         return  _ptr[n];     } private :    T* _ptr; }; 
自己也实现一个简单的weak_ptr,还是用相同的代码进行测试,可以看到,成功进行了析构
1 2 3 4 5 6 7 8 9 10 [ ListNode()] 00C903C8 [init] 00C903C8 [ ListNode()] 00C90128 [init] 00C90128 [des] pcount:0 ptr:00C90128 [des] delete 00C90128 [~ListNode()] 00C90128 [des] pcount:0 ptr:00C903C8 [des] delete 00C903C8 [~ListNode()] 00C903C8 
7.5 成员函数 
对库中实现的weak_ptr 的成员函数做一定解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void  swap  (weak_ptr& x)  noexcept void  reset ()  noexcept long  int  use_count ()  const  noexcept bool  expired ()  const  noexcept shared_ptr<element_type> lock ()  const  noexcept  ;template  <class  U > bool  owner_before  (const  weak_ptr<U>& x)  const template  <class  U > bool  owner_before  (const  shared_ptr<U>& x)  const 
7.6 weak_ptr和引用计数问题 
上文对shared_ptr和weak_ptr的实现只是最基础的一个处理,还有很多问题没有解决
std中的weak_ptr有一个函数是expired(),如果weak_ptr内部不存在引用计数,它就没有办法判断托管的对象是否已经过期。所以答案很明确了,weak_ptr里面肯定是有一个引用计数的!
这就会引出另外一个问题,我们上面实现的shared_ptr的代码就包含了这个问题。
如果shared_ptr析构的时候已经将引用计数给delete了,weak_ptr还怎么判断? 
 
其实解决它并不难:继续利用引用计数的思路,给shared_ptr的引用计数再上一个引用计数。这里将share_ptr的引用计数记为count(资源被引用的计数),对count的引用计数记为weak(引用计数的计数);
当shared_ptr出现拷贝的时候,同时操作count和weak(都会加加); 
当从shared_ptr拷贝给weak_ptr,或者weak_ptr之间拷贝,给weak加加; 
当shared_ptr销毁的时候,同时操作count和weak(都会减减); 
当count为0的时候,销毁托管的资源; 
当weak为0的时候,销毁count(由weak_ptr和shared_ptr同时来管理); 
 
这时候就需要两把锁了,一个用来锁weak一个用来锁count ,weak_ptr在拷贝的时候只操作weak锁,在调用expired()函数的时候会调用count锁来判断count是否为0。
如果是自主实现,可以直接使用cpp里面提供的atmoic原子变量来操作,避免定义多个锁的麻烦。这里可以看看精简后的mvcc源码:https://zhuanlan.zhihu.com/p/680068428 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template  <typename  Ty>struct  RefCount  {    std::atomic_int32_t  _uses = 1 ;     std::atomic_int32_t  _weaks = 1 ;     Ty *_ptr; }; template  <class  Ty >struct  Ptr_base  {    friend  class  shared_ptr <Ty>;     friend  class  weak_ptr <Ty>;     Ty *_ptr = nullptr ;     RefCount<Ty> *_ref = nullptr ;  } 
注意到这里,_ptr 存了两份,一个在count里面一个就在ptrBase里面,这是为了方便直接get(),少一次通过RefCount的内存访问。
8.定制删除器 
对于我们自己写的shared_ptr,有一个问题就是,析构的时候,默认是写死的delete;
如果用户传入的是一个数组new int[10],此时delete就不对应(需要用delete[]),会出现问题(但不一定会报错)
还有些情况,我们想给智能指针传入一个文件指针,此时就不能用delete来进行资源释放了。为了避免这些情况,智能指针引入了定制删除器 
8.1 unique_ptr和shared_ptr的不同用法 
1 2 3 4 template  <class  T , class  D  = default_delete<T>> class  unique_ptr;template  <class  T , class  D > class  unique_ptr <T[],D>;
比如库中unique_ptr的模板参数中,就有一个模板参数D用于接收用户传入的删除器。而shared_ptr则是采用在构造函数中传入删除器对象的方式来实现定制删除
1 2 3 template  <class  U , class  D > shared_ptr  (U* p, D del);template  <class  D > shared_ptr  (nullptr_t  p, D del);
库中默认的删除器如下
1 2 3 4 template  <class  T > class  default_delete ;template  <class  T > class  default_delete <T[]>;
内部采用了重载操作符()的办法,来实现仿函数。默认情况下,使用的是delete,如果指定了是数组类型,则会使用delete[]
如果我们要处理的是文件指针或者malloc的值,就只需要自己传入一个仿函数(定制删除器)即可
8.2 使用 
这里我定制了一个free的删除器,和文件指针fclose的删除器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 template <class  T >struct  Free {     void  operator () (T* ptr)       {        cout << "[free]  "  << ptr << endl;         free (ptr);     } }; struct  Fclose {     void  operator () (FILE* ptr)       {        cout << "[fclose] "  << ptr << endl;         fclose (ptr);     } }; 
用如下代码进行测试
1 2 3 4 5 6 7 unique_ptr<int , default_delete<int []>> up1 (new  int [10 ]); unique_ptr<ListNode<int >, default_delete<ListNode<int >[]>> up2 (new  ListNode<int >[2 ]); unique_ptr<FILE, Fclose> up3 ((FILE*)fopen("test.cpp" , "r" ))  ;unique_ptr<ListNode<int >, Free<ListNode<int >>> up4 ((ListNode<int >*)malloc (sizeof (ListNode<int >))); 
可以看到,成功进行了ListNode数组的销毁,以及malloc、文件指针的free、关闭操作
1 2 3 4 5 6 [ ListNode()] 011D5B7C [ ListNode()] 011D5B88 [free]  011E1070 [fclose] 011D89A8 [~ListNode()] 011D5B88 [~ListNode()] 011D5B7C 
需要注意的是,malloc创建的ListNode<int>空间并不会调用ListNode的构造函数,free也不会调用析构函数
因为shared_ptr需要采用类对象的方式在构造函数中进行传参。这方面的底层实现有些复杂。随之而来的好处就是我们可以直接传入一个lambda表达式来作为删除器,避免了代码冗长之后,找不到想要的删除器的定义的问题。
1 2 3 4 5 default_delete<ListNode<int >[]> d1; shared_ptr<ListNode<int >> sp1 (new  ListNode<int >[3 ], d1); cout << "###########################"  << endl; shared_ptr<ListNode<int >> sp2 (new  ListNode<int >[3 ], [](ListNode<int >* ptr){delete [] ptr; }); 
运行结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 [ ListNode()] 0127F1BC [ ListNode()] 0127F1C8 [ ListNode()] 0127F1D4 ########################### [ ListNode()] 01278D4C [ ListNode()] 01278D58 [ ListNode()] 01278D64 [~ListNode()] 01278D64 [~ListNode()] 01278D58 [~ListNode()] 01278D4C [~ListNode()] 0127F1D4 [~ListNode()] 0127F1C8 [~ListNode()] 0127F1BC 
9.总结 
智能指针 
拷贝特点 
定制删除器 
 
 
auto_ptr(C++98) 
管理权转移。复制后,原有对象失效,使用原有对象会段错误。 
- 
 
unique_ptr(scpoed_ptr) 
禁止拷贝,但是可以被move。 
使用模板参数来传入定制删除器 
 
shared_ptr 
支持拷贝,采用引用计数,但是会有循环引用问题。 
在构造函数中传入删除器对象 
 
weak_ptr 
支持拷贝,只用于托管指针,不参与指针空间的释放,不计入引用计数。用于解决循环引用问题。(注意不计入引用计数不代表没有引用计数) 
- 
 
 
The end 
智能指针的基本用法到这里就over了,了解智能指针的同时,需要熟知RAII思想。在不少类的设计中,都会用到这个思想。