引言 C++ 中经常需要 new 一个对象,开辟一个内存空间,返回一个指针来操作这个内存。使用完毕之后,需要通过 delete 来释放内存空间。如果内存没有释放,那这块内存将无法再利用,导致内存泄漏。为降低人为疏忽,C++ 11 的新特性中引入了三种智能指针,来自动化地管理内存资源:
unique_ptr: 管理的资源唯一的属于一个对象,但是支持将资源移动给其他 unique_ptr 对象。当拥有所有权的 unique_ptr 对象析构时,资源即被释放。
shared_ptr: 管理的资源被多个对象共享,内部采用引用计数跟踪所有者的个数。当最后一个所有者被析构时,资源即被释放。
weak_ptr: 与 shared_ptr 配合使用,虽然能访问资源但却不享有资源的所有权,不影响资源的引用计数。有可能资源已被释放,但 weak_ptr 仍然存在。因此每次访问资源时都需要判断资源是否有效。
本文主要在循环引用的场景下探讨 shard_ptr 和 weak_ptr 原理。
循环引用 shared_ptr 通过引用计数的方式管理内存,当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他的 shared_ptr 指向相同的对象,当引用计数为 0 时,内存将被自动释放。
1 2 auto p = make_shared <int >(10 ); auto q (p) ;
当对 shared_ptr 赋予新值,或被销毁时,引用计数会递减。
1 2 auto r = make_shared <int >(20 ); r = q;
通常情况下 shared_ptr 可以正常运转,但是在循环引用的场景下,shared_ptr 无法正确释放内存。循环引用,顾名思义,A 指向 B,B 指向 A,在表示双向关系时,是很可能出现这种情况的,例如:
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 #include <iostream> #include <memory> using namespace std;class Son ;class Father {public : shared_ptr<Son> son_; Father () { cout << __FUNCTION__ << endl; } ~Father () { cout << __FUNCTION__ << endl; } }; class Son {public : shared_ptr<Father> father_; Son () { cout << __FUNCTION__ << endl; } ~Son () { cout << __FUNCTION__ << endl; } }; int main () { auto son = make_shared <Son>(); auto father = make_shared <Father>(); son->father_ = father; father->son_ = son; cout << "son: " << son.use_count () << endl; cout << "father: " << father.use_count () << endl; return 0 ; }
程序的执行结果如下:
Son
Father
son: 2
father: 2
可以看到,程序分别执行了 Son 和 Father 的构造函数,但是没有执行析构函数,出现了内存泄漏。
shared_ptr 原理 shared_ptr 实际上是对裸指针进行了一层封装,成员变量除了裸指针外,还有一个引用计数,它记录裸指针被引用的次数(有多少个 shared_ptr 指向这同一个裸指针),当引用计数为 0 时,自动释放裸指针指向的资源。影响引用次数的场景包括:构造、赋值、析构。基于三个最简单的场景,实现一个 demo 版 shared_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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> #include <memory> using namespace std;template <typename T>class SharedPtr {public : int * counter; T* resource; SharedPtr (T* resc = nullptr ) { cout << __PRETTY_FUNCTION__ << endl; counter = new int (1 ); resource = resc; } SharedPtr (const SharedPtr& rhs) { cout << __PRETTY_FUNCTION__ << endl; resource = rhs.resource; counter = rhs.counter; ++*counter; } SharedPtr& operator =(const SharedPtr& rhs) { cout << __PRETTY_FUNCTION__ << endl; --*counter; if (*counter == 0 ) { delete counter; delete resource; } resource = rhs.resource; counter = rhs.counter; ++*counter; } ~SharedPtr () { cout << __PRETTY_FUNCTION__ << endl; --*counter; if (*counter == 0 ) { delete counter; delete resource; } } int use_count () { return *counter; } };
在循环引用示例中,用到了 make_shared 函数:
1 auto son = make_shared <Son>();
此用法等价于:
1 2 auto son_ = new Son (); shared_ptr<Son> son (son_) ;
代入 SharedPtr 的实现来分析示例中 main 函数的执行过程,可以得到:
1 2 3 4 auto son = make_shared <Son>(); auto father = make_shared <Father>(); son->father_ = father; father->son_ = son;
当 main 函数执行完时,执行析构函数,此时由于 son.counter=1,father.couter=1,不满足 if 条件,不会实行 delete 命令完成资源释放,导致内存泄漏。
weak_ptr 原理 为解决循环引用的问题,仅使用 shared_ptr 是无法实现的。堡垒无法从内部攻破的时候,需要借助外力,于是有了 weak_ptr,字面意思是弱指针。为啥叫弱呢?shared_ptr A 被赋值给 shared_ptr B 时,A 的引用计数加 1;shared_ptr A 被赋值给 weak_ptr C 时,A 的引用计数不变。引用力度不够强,不足以改变引用计数,所以就弱了(个人理解,有误请指正)。
weak_ptr 在使用时,是与 shared_ptr 绑定的。基于 SharedPtr 实现来实现 demo 版的 WeakPtr,并解决循环引用的问题,全部代码如下:
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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #include <iostream> #include <memory> using namespace std;template <typename T>class SharedPtr {public : int * counter; int * weakref; T* resource; SharedPtr (T* resc = nullptr ) { cout << __PRETTY_FUNCTION__ << endl; counter = new int (1 ); weakref = new int (0 ); resource = resc; } SharedPtr (const SharedPtr& rhs) { cout << __PRETTY_FUNCTION__ << endl; resource = rhs.resource; counter = rhs.counter; ++*counter; } SharedPtr& operator =(const SharedPtr& rhs) { cout << __PRETTY_FUNCTION__ << endl; --*counter; if (*counter == 0 ) { delete counter; delete resource; } resource = rhs.resource; counter = rhs.counter; ++*counter; } ~SharedPtr () { cout << __PRETTY_FUNCTION__ << endl; --*counter; if (*counter == 0 ) { delete counter; delete resource; } } int use_count () { return *counter; } }; template <typename T>class WeakPtr {public : T* resource; WeakPtr (T* resc = nullptr ) { cout << __PRETTY_FUNCTION__ << endl; resource = resc; } WeakPtr& operator =(SharedPtr<T>& ptr) { cout << __PRETTY_FUNCTION__ << endl; resource = ptr.resource; ++*ptr.weakref; } ~WeakPtr () { cout << __PRETTY_FUNCTION__ << endl; } }; class Son ;class Father {public : SharedPtr<Son> son_; Father () { cout << __PRETTY_FUNCTION__ << endl; } ~Father () { cout << __PRETTY_FUNCTION__ << endl; } }; class Son {public : WeakPtr<Father> father_; Son () { cout << __PRETTY_FUNCTION__ << endl; } ~Son () { cout << __PRETTY_FUNCTION__ << endl; } }; int main () { auto son_ = new Son (); auto father_ = new Father (); SharedPtr<Son> son (son_) ; SharedPtr<Father> father (father_) ; son.resource->father_ = father; father.resource->son_ = son; cout << "son: " << son.use_count () << endl; cout << "father: " << father.use_count () << endl; return 0 ; }
代码执行结果如下:
WeakPtr::WeakPtr(T*) [with T = Father]
Son::Son()
SharedPtr::SharedPtr(T*) [with T = Son]
Father::Father()
SharedPtr::SharedPtr(T*) [with T = Son]
SharedPtr::SharedPtr(T*) [with T = Father]
WeakPtr& WeakPtr::operator=(SharedPtr&) [with T = Father]
SharedPtr& SharedPtr::operator=(const SharedPtr&) [with T = Son]
son: 2
father: 1
SharedPtr::~SharedPtr() [with T = Father]
Father::~Father()
SharedPtr::~SharedPtr() [with T = Son]
SharedPtr::~SharedPtr() [with T = Son]
Son::~Son()
WeakPtr::~WeakPtr() [with T = Father]
可以看到 Son 对象和 Father 对象均被析构,内存泄漏的问题得到解决。析构过程解读如下:
SharedPtr::~SharedPtr() [with T = Father] # 析构 father,由于 father.couter=1,减 1 后执行 delete father_
Father::~Father() # 析构 father_,执行~Father(),进一步析构成员变量
SharedPtr::~SharedPtr() [with T = Son] # 析构 SharedPtr,此时 son.couter 减 1,son.counter=1
SharedPtr::~SharedPtr() [with T = Son] # 析构 son,由于 son.counter=1,减 1 后执行 delete son_
Son::~Son() # 析构 son_,执行~Son(),进一步析构成员变量
WeakPtr::~WeakPtr() [with T = Father] # 析构 WeakPtr
总结
尽量使用智能指针管理资源申请与释放,减少人为 new 和 delete 误操作和考虑不周的问题。
使用 make_shared 来创建 shared_ptr,如果先 new 一个对象,再用这个对象的裸指针构造一个 shared_ptr 指针,可能出现问题。shared_ptr 会自动释放资源,如果再手动 delete,释放两次那就挂了。