前言
在 C 语言中,动态内存管理一直是一个非常头疼的问题,你需要自己申请内存与释放内存,一不小心就容易触发内存泄露、指针悬挂、重复释放等问题,这些问题可能还是隐性的,不容易排查。在 C++ 11
引入的三种智能指针 shared_ptr
、unique_ptr
、weak_ptr
用于自动管理动态内存,当然不规范的使用它们依然会导致一些问题。值得注意的是,在更早的 C++
版本中的 auto_ptr
在 C++ 11
开始被弃用,在 C++ 17
被移除,你可以使用 unique_ptr
来替代它,本文不会讲解。
本文将从两个方面介绍 shared_ptr
、unique_ptr
、weak_ptr
三种智能指针,这包括:规范使用智能指针、智能指针原理和代码复现。
shared_ptr 共享指针
创建共享指针
shared_ptr
可以使用下面的四种方法构造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void deleter (int *p) { delete p; cout << "Free " << p << endl; } int main () { std::shared_ptr<int > sp; auto sp = std::make_shared <int >(10 ); std::shared_ptr<int > sp (new auto (10 )) ; std::shared_ptr<int > sp (new auto (10 ), deleter) ; return 0 ; }
自定义析构器通常在两种情形下使用:一是确实需要重写析构器,进行其他相关操作;二是与 C 语言链接,在 C 语言中没有析构函数,这可能需要用户来绑定析构函数。
共享指针相关方法
在介绍 shared_ptr
相关方法前有必要首先简单介绍其基本原理。shared_ptr
用于在不同变量间共享资源,其底层维护了一组绑定的指针和计数器,计数器维护了其被多少变量共享,当数量为 0
时,底层指针指向的内存将被释放。
方法
功能
sp.get()
获取底层指针,返回 T *
。这个操作是危险的。
sp.reset()
解除底层指针的绑定,计数器自减。
std::swap(sp1, sp2)
交换底层指针的绑定,不影响计数器。
sp.use_count()
获取计数器计数,返回 long
。
sp.unique()
是否是独占的,即计数器是否为 1
,返回 bool
。
static_cast<bool>(sp)
是否绑定底层指针,shared_ptr
默认初始化时或 reset
后底层指针为 NULL
。
*sp
/ sp->
返回底层指针的解引用,此操作前用户应该确保 sp
绑定非 NULL
。
sp1 = sp2
将 sp2
的底层指针赋值到 sp1
,进行相应计数。
sp1 = std::move(sp2)
将 sp2
的底层指针移动到 sp1
,进行相应计数。
这里需要注意,移动语义与赋值语义在 shared_ptr
中是不同的(在 unique_ptr
中也不同),移动语义在完成赋值后将 reset
原智能指针。
规范使用 shared_ptr
智能指针的初衷是为用户提供自动的动态内存管理,同时也保证了程序安全。但是,如果智能指针底层指针泄露到外部,同样存在安全风险。因此,永远不要同时使用底层指针和智能指针。下面我们将辨析一些可能存在风险的操作:
1 2 3 4 int *p = new auto (10 );std::shared_ptr<int > sp (p) ;
这也是为什么我们之前说 std::make_shared
是更安全的方案。当然,如果你像下面一样书写也是安全的。
1 2 std::shared_ptr<int > sp (new auto (10 )) ;
1 2 3 4 auto sp = std::make_shared <int >(10 );int *p = sp.get ();
在这里 .get
获取了 sp
的底层指针,而用户将它储存下来。这样的作法是危险的,.get
提供了一个危险的接口,它的返回值应该被立即使用,例如用于打印等等。如果你确实需要存储这个返回值,p
应该存在于尽量短小的生命周期。
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 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 #include <bits/stdc++.h> using std::cin;using std::cout;using std::endl;template <typename T>class SharedPtr { private : size_t *count = NULL ; T *ptr = NULL ; public : SharedPtr (T *p = NULL ) { if (p == NULL ) return ; this ->count = new size_t (1 ); this ->ptr = p; } SharedPtr &operator =(const SharedPtr &sp) { if (this == &sp) return *this ; this ->reset (); this ->count = sp.count; this ->ptr = sp.ptr; if (this ->count) ++*this ->count; return *this ; } SharedPtr &operator =(SharedPtr &&sp) { if (this == &sp) return *this ; this ->reset (); this ->count = sp.count; this ->ptr = sp.ptr; sp.count = NULL ; sp.ptr = NULL ; return *this ; } void reset () { if (this ->ptr == NULL ) return ; if (--*this ->count == 0 ) { delete this ->count; delete this ->ptr; } this ->count = NULL ; this ->ptr = NULL ; } SharedPtr (const SharedPtr &sp) { this ->operator =(sp); } SharedPtr (SharedPtr &&sp) { this ->operator =(std::move (sp)); } operator bool () const { return this ->ptr; } T &operator *() { return *this ->ptr; } T *operator ->() { return this ->ptr; } T *get () const { return this ->ptr; } void swap (SharedPtr &sp) { std::swap (this ->count, sp.count); std::swap (this ->ptr, sp.ptr); } bool unique () const { return this ->count ? *this ->count == 1 : false ; } long use_count () const { return this ->count ? *this ->count : 0 ; } ~SharedPtr () { this ->reset (); } }; int main () { SharedPtr<int > sp1 (new auto (10 )) ; SharedPtr<int > sp2 (new auto (11 )) ; auto sp3 = sp1; auto sp4 = sp2; cout << sp1.use_count () << ' ' << sp2.use_count () << ' ' << sp3.use_count () << ' ' << sp4.use_count () << endl; sp1 = SharedPtr <int >(std::move (sp2)); cout << sp1.use_count () << ' ' << sp2.use_count () << ' ' << sp3.use_count () << ' ' << sp4.use_count () << endl; std::swap (sp3, sp4); cout << sp1.use_count () << ' ' << sp2.use_count () << ' ' << sp3.use_count () << ' ' << sp4.use_count () << endl; sp3.reset (); cout << sp1.use_count () << ' ' << sp2.use_count () << ' ' << sp3.use_count () << ' ' << sp4.use_count () << endl; return 0 ; }
unique_ptr 独占指针
创建独占指针
与共享指针不同的是,独占指针 unique_ptr
不允许拷贝,它保证了至多只有一个智能指针占有这份资源。
1 2 3 4 5 6 7 8 9 10 11 12 void deleter (int *p) { delete p; cout << "Free " << p << endl; } int main () { std::unique_ptr<int > up; auto up = std::make_unique <int >(10 ); std::unique_ptr<int > up (new auto (10 )) ; std::unique_ptr<int , void (*) (int *) > up (new auto (10 ), deleter) ; return 0 ; }
这里我们注意到,对于unique_ptr
使用自定义析构器,我们必须传入析构函数模板,这与 unique_ptr
的实现原理有关。当然针对这个问题我们可以使用别名简化:
1 2 3 4 5 6 7 8 9 namespace std { template <typename T> using UniquePtr = unique_ptr<T, void (*)(T *)>; } int main () { std::UniquePtr<int > up (new auto (10 ), deleter) ; return 0 ; }
独占指针相关方法
与共享指针不同的是,unique_ptr
由于是独占的,不需要维护计数器。同时它不提供拷贝构造函数与拷贝赋值函数,从而把保证独占资源。
方法
功能
up.get()
获取底层指针,返回 T *
。这个操作是危险的。
up.reset()
接触底层指针的绑定,释放底层内存。
up.release()
接触底层指针的绑定,返回底层指针。不获取返回值将产生内存泄露。
std::swap(up1, up2)
交换底层指针的绑定。
static_cast<bool>(up)
是否绑定底层指针,unique_ptr
默认初始化时或 reset
后底层指针为 NULL
。
*up
/ up->
返回底层指针的解引用,此操作前用户应该确保 up
绑定非 NULL
。
up1 = std::move(up2)
将 up2
的底层指针移动到 up1
。
规范使用 unique_ptr
在使用 unique_ptr
时,不仅需要注意和 shared_ptr
相同的安全问题,还需要注意 release
方法存在的问题。
如果用户不接收 .release
的返回值将会造成内存泄露,因为 release
是进行指针所有权的转移,而不会释放底层内存。例如下面这个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <bits/stdc++.h> using std::cin;using std::cout;using std::endl;namespace std { template <typename T> using UniquePtr = unique_ptr<T, void (*)(T *)>; } void deleter (int *p) { delete p; cout << "Free " << p << endl; } int main () { std::UniquePtr<int > up (new auto (10 ), deleter) ; up.release (); return 0 ; }
注意,这里我们使用了自定义析构器,如果内存是被 unique_ptr
释放的,那么将会输出 Free ...
。而这里没有输出,说明产生了内存泄露。为了解决这个,每次使用 release
时一定要接收其返回值,这里可以使用底层指针接收,也可以使用独占指针接收。当然,我更推荐的做法是使用 unique_ptr
的移动语义,这样一定不会出现上面讨论的问题。
unique_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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <bits/stdc++.h> using std::cin;using std::cout;using std::endl;template <typename T>class UniquePtr { private : T *ptr = NULL ; public : UniquePtr (T *p = NULL ) : ptr (p) {} UniquePtr &operator =(UniquePtr &&up) { if (this == &up) this ->reset (); this ->ptr = up.ptr; up.ptr = NULL ; return *this ; }; void reset () { if (this ->ptr) delete this ->ptr; this ->ptr = NULL ; } T *release () { T *p = this ->ptr; this ->ptr = NULL ; return p; } UniquePtr (UniquePtr &&up) { this ->operator =(std::move (up)); } operator bool () const { return this ->ptr; } T &operator *() { return *this ->ptr; } T *operator ->() { return this ->ptr; } T *get () const { return this ->ptr; } void swap (UniquePtr &up) { std::swap (this ->ptr, up.ptr); } ~UniquePtr () { this ->reset (); } UniquePtr (const UniquePtr &up) = delete ; UniquePtr &operator =(const UniquePtr &up) = delete ; }; int main () { UniquePtr<int > up1 (new auto (10 )) ; UniquePtr<int > up2 (new auto (11 )) ; cout << (up1 ? std::to_string (*up1) : "NULL" ) << ' ' << (up2 ? std::to_string (*up2) : "NULL" ) << endl; up2 = std::move (up1); cout << (up1 ? std::to_string (*up1) : "NULL" ) << ' ' << (up2 ? std::to_string (*up2) : "NULL" ) << endl; std::swap (up1, up2); cout << (up1 ? std::to_string (*up1) : "NULL" ) << ' ' << (up2 ? std::to_string (*up2) : "NULL" ) << endl; return 0 ; }
weak_ptr 弱指针
创建弱指针
弱指针是辅助于 shared_ptr
的存在,它指向一个 shared_ptr
,并对其计数器不产生影响。weak_ptr
依赖于 shared_ptr
而存在,并且不直接参与自动内存管理,因此被称为弱指针。weak_ptr
主要用于解决循环引用的问题。
1 2 3 4 5 6 int main () { std::shared_ptr<int > sp (new auto (10 )) ; std::weak_ptr<int > wp; std::weak_ptr<int > wp (sp) ; return 0 ; }
弱指针相关方法
从这张表也可以看出,weak_ptr
提供的方法也非常 “弱”,它没有 operator bool
/ operator*
/ operator->
这些方法,所有值访问都通过 .lock
返回的 shared_ptr
间接完成。
方法
功能
wp.lock()
获取 weak_ptr
底层 shared_ptr
。如果底层已弃用,返回包含 NULL
的 shared_ptr
。
wp.reset()
解除与底层 shared_ptr
的绑定。
std::swap(wp1, wp2)
交换底层 shared_ptr
的绑定。
wp.use_count()
获取底层 shared_ptr
计数器计数,返回 long
。
wp.expired()
是否底层已弃用,即计数器是否为 0
,返回 bool
。
wp = sp
/ wp1 = wp2
weak_ptr
的拷贝赋值函数和移动赋值函数是相同的。
注意,weak_ptr
未绑定 shared_ptr
的情况与底层已弃用被视为是相同的。weak_ptr
其底层事实上并非指向 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 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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 #include <bits/stdc++.h> using std::cin;using std::cout;using std::endl;template <typename T, typename Count>class SharedPtr ;template <typename T>class WeakPtr ;template <typename T, typename Counter = SharedPtr<size_t , size_t *>>class SharedPtr { friend class WeakPtr<T>; private : Counter count = NULL ; T *ptr = NULL ; void delete_count () { static_assert ( std::is_same<Counter, size_t *>::value or std::is_same<Counter, SharedPtr<size_t , size_t *>>::value, "Counter of SharedPtr must be either `size_t *` or `SharedPtr<size_t, size_t *>`." ); if constexpr (std::is_same<Counter, size_t *>::value) { if (*this ->count == 0 ) delete this ->count; this ->count = NULL ; } else if constexpr (std::is_same<Counter, SharedPtr<size_t , size_t *>>::value) { this ->count.reset (); } } public : void reset () { if (this ->ptr == NULL ) return ; if (--*this ->count == 0 ) { delete this ->ptr; } this ->ptr = NULL ; this ->delete_count (); } }; template <typename T>class WeakPtr { private : SharedPtr<size_t , size_t *> count = NULL ; T *ptr = NULL ; public : WeakPtr () = default ; WeakPtr (const SharedPtr<T> &sp) { this ->operator =(sp); } WeakPtr (SharedPtr<T> &&sp) : WeakPtr (sp) {} WeakPtr (const WeakPtr &) = default ; WeakPtr (WeakPtr &&) = default ; WeakPtr &operator =(const SharedPtr<T> &sp) { this ->count = sp.count; this ->ptr = sp.ptr; return *this ; } WeakPtr &operator =(SharedPtr<T> &&sp) { return this ->operator =(sp); } WeakPtr &operator =(const WeakPtr &) = default ; WeakPtr &operator =(WeakPtr &&) = default ; void reset () { this ->count = NULL ; this ->ptr = NULL ; } SharedPtr<T> lock () const { if (this ->expired ()) return NULL ; return this ->ptr; } void swap (WeakPtr &wp) { std::swap (this ->count, wp.count); std::swap (this ->ptr, wp.ptr); } bool expired () const { if (this ->count == NULL ) return true ; if (*this ->count == 0 ) { const_cast <WeakPtr *>(this )->reset (); return true ; } return false ; } long use_count () const { return this ->expired () ? 0 : *this ->count; } }; int main () { SharedPtr<int > sp1 (new auto (10 )) ; SharedPtr<int > sp2 (new auto (11 )) ; auto sp3 = sp1; auto sp4 = sp2; WeakPtr<int > wp1 (sp1) ; WeakPtr<int > wp2 (sp2) ; cout << wp1.use_count () << ' ' << wp2.use_count () << endl; sp1 = SharedPtr <int >(std::move (sp2)); cout << wp1.use_count () << ' ' << wp2.use_count () << endl; std::swap (sp3, sp4); cout << wp1.use_count () << ' ' << wp2.use_count () << endl; sp3.reset (); cout << wp1.use_count () << ' ' << wp2.use_count () << endl; sp1.reset (); cout << wp1.use_count () << ' ' << wp2.use_count () << endl; sp2.reset (); cout << wp1.use_count () << ' ' << wp2.use_count () << endl; sp3.reset (); cout << wp1.use_count () << ' ' << wp2.use_count () << endl; sp4.reset (); cout << wp1.use_count () << ' ' << wp2.use_count () << endl; return 0 ; }
shared_ptr 与 make_shared 的区别
我们在 weak_ptr
的章节讲解 shared_ptr
与 make_shared
方法的区别,自然是因为两者在使用 weak_ptr
时存在区别。
前面讲到,我们可以使用 shared_ptr
或 make_shared
两种方法构造 shared_ptr
对象。我们建议用户使用 make_shared
构造对象,因为 make_shared
避免了暴露指针带来的安全风险。但是另一方面,make_shared
存在比 shared_ptr
更好的效率,两者在底层内存结构也是不同的。
可以参考下面这两篇文章:
why make_shared ? - Bitdewy
std::make_shared - cppreference.com