终于来填坑了😂
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思想。在不少类的设计中,都会用到这个思想。