从一次 double free 深入理解 shared_ptr 的原理与最佳实践
在使用 C++ 开发过程中,最容易也是最麻烦的问题便是内存泄漏。相较于 Java、Python 或者 Go 语言都拥有垃圾回收机制,在对象没有引用时就会被系统自动回收而且基本上没有指针的概念,但是 C++ 则要求程序员自己管理内存,这一方面让程序员有更大的自由度但是也会很大影响程序员的开发效率。因此 C++11 标准中新推出了 shared_ptr
、unique_ptr
和 weak_ptr
三个智能指针来帮助管理内存。
智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源,所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放。
笔者在排查一个 double free 问题时,重新回顾了 shared_ptr
的工作原理,以及列出一些注意事项,本文着重介绍 shared_ptr
,其他智能指针不过多赘述。
shared_ptr
本质shared_ptr
能够自动记录共享对象的引用次数,并且在引用计数降至 $0$ 时自动删除对象,从而防止内存泄漏。每个 shared_ptr
的拷贝都指向相同的内存,在最后一个 shared_ptr
析构的时候其指向的内存资源才会被释放。
本质上 shared_ptr
是有两层析构 :
shared_ptr
本身析构会使得指向的共享对象的引用数 $-1$,当共享对象引用数为 $0$ 时,则调用共享对象本身的析构函数
这样就可以理解循环引用了:共享对象引用还是 $1$ 时,未调用共享对象本身的析构函数,其中成员 shared_ptr
的析构函数也不会被调用
shared_ptr
初始化方式:
构造函数
std::make_shared()
辅助函数
reset()
1 2 3 4 5 6 7 std::shared_ptr<int > p (new int (1 )) ;std::shared_ptr<int > p2 = p; std::shared_ptr<A> ap = std::make_shared <A>(); std::shared_ptr<int > ptr; ptr.reset (new int (1 ));
不能将一个原始指针直接赋值给一个智能指针,如:std::shared_ptr<int> p = new int(1)
。
对于一个未初始化的智能指针,可以通过调用 reset
方法初始化,当智能指针中有值的时候,调用 reset
方法会使引用计数减 $1$。当需要获取原指针的时候可以通过 get
方法返回原始指针:
1 2 std::shared_ptr<int > p (new int (1 )) ;int *ptr = p.get ();
智能指针初始化时也可以指定删除器,当其引用计数为 $0$ 时将自动调用删除器来释放对象,删除器可以是一个函数对象。
比如当使用 shared_ptr
管理动态数组时,需要指定删除器,因为 shared_ptr
默认删除器不支持数组对象 :
1 2 std::shared_ptr<int > p (new int [10 ], [](int *p) { delete []p; })
shared_ptr
注意事项关于 shared_ptr
的注意事项:
不要用一个裸指针初始化多个 shared_ptr
,会出现 double_free 导致程序崩溃
通过 shared_from_this()
返回 this 指针,不要把 this 指针作为 shared_ptr
返回出来,因为 this
指针本质就是裸指针,通过 this 返回可能会导致重复析构,不能把 this 指针交给智能指针管理 。
尽量使用 std::make_shared<T>()
,少用 new
不要 delete
get()
返回的裸指针
不是 new
出来的空间要自定义删除器
要避免循环引用 ,循环引用导致内存永远不会被释放,造成内存泄漏(不在赘述)
1. 不要用一个裸指针初始化多个 shared_ptr
(会导致 double free) 问题场景:
1 2 3 int * raw_ptr = new int (42 );std::shared_ptr<int > sp1 (raw_ptr) ;std::shared_ptr<int > sp2 (raw_ptr) ;
两个独立的 shared_ptr
会各自维护 一个引用计数控制块(相互独立)
当 sp1
和 sp2
销毁时都会尝试释放 raw_ptr
,导致 双重释放 (double free)
结果通常是程序崩溃或未定义行为
正确做法:
1 2 3 4 5 6 7 8 9 auto sp1 = std::make_shared <int >(42 );auto sp2 = sp1; int * raw_ptr = new int (42 );std::shared_ptr<int > sp1 (raw_ptr) ;std::shared_ptr<int > sp2 = sp1;
2. 正确使用 shared_from_this()
而不是直接返回 this
指针 问题场景:
1 2 3 4 5 6 7 8 9 class BadExample {public : std::shared_ptr<BadExample> get_this () { return std::shared_ptr <BadExample>(this ); } }; auto obj = std::make_shared <BadExample>();auto another_ref = obj->get_this ();
这会创建两个独立的 shared_ptr
控制块
当两个 shared_ptr
销毁时都会尝试析构同一个对象
正确做法:
1 2 3 4 5 6 7 8 9 class GoodExample : public std::enable_shared_from_this<GoodExample> {public : std::shared_ptr<GoodExample> get_this () { return shared_from_this (); } }; auto obj = std::make_shared <GoodExample>();auto another_ref = obj->get_this ();
3. 优先使用 std::make_shared<T>()
而不是 new
问题场景:
1 2 3 4 5 std::shared_ptr<MyClass> sp (new MyClass(arg1, arg2)) ;auto sp = std::make_shared <MyClass>(arg1, arg2);
优势:
性能更好:单次内存分配(对象 + 控制块)
异常安全:不会在 new
和 shared_ptr
构造之间发生泄漏
代码更简洁:不需要重复类型名称
缓存友好:对象和控制块内存相邻
例外情况:
4. 不要 delete
get()
返回的裸指针 问题场景:
1 2 3 4 5 auto sp = std::make_shared <int >(42 );int * raw_ptr = sp.get ();delete raw_ptr;
shared_ptr
仍然拥有内存所有权
手动 delete
会导致:
double free
控制块状态不一致
未定义行为(通常崩溃)
正确做法:
1 2 3 auto sp = std::make_shared <int >(42 );int * raw_ptr = sp.get ();
5. 非 new
分配的内存需要自定义删除器 问题场景:
1 2 3 4 5 6 7 void * mem = malloc (1024 );std::shared_ptr<void > sp (mem) ; FILE* fp = fopen ("file.txt" , "r" ); std::shared_ptr<FILE> sp (fp) ;
正确做法:
1 2 3 4 5 6 7 8 9 10 void * mem = malloc (1024 );std::shared_ptr<void > sp (mem, free) ; FILE* fp = fopen ("file.txt" , "r" ); std::shared_ptr<FILE> sp (fp, [](FILE* f) { fclose(f); }) ;int * arr = new int [10 ];std::shared_ptr<int > sp (arr, [](int * p) { delete [] p; }) ;
常见删除器场景:
C 风格内存分配(malloc/calloc/realloc
)→ 使用 free
文件操作(fopen
)→ 使用 fclose
系统资源(套接字、句柄等)→ 使用对应的释放函数
数组 → 使用 delete[]
6. 避免循环引用导致的内存泄露 问题场景 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class A ;class B ;class A {public : std::shared_ptr<B> b; }; class B {public : std::shared_ptr<A> a; }; int main () { std::shared_ptr<A> ap = std::make_shared <A>(); std::shared_ptr<B> bp = std::make_shared <B>(); ap->b = bp; bp->a = ap; return 0 ; }
问题场景 2 1 2 3 4 5 6 7 8 9 10 11 class Node {public : std::shared_ptr<Node> next; std::shared_ptr<Node> prev; ~Node () { std::cout << "Node destroyed\n" ; } }; auto node1 = std::make_shared <Node>();auto node2 = std::make_shared <Node>();node1->next = node2; node2->prev = node1;
当 node1
和 node2
离开作用域时:
node1
的引用计数从 1→0?不,因为 node2->prev
还持有引用(实际从 2→1)
node2
的引用计数同样从 2→1
结果:两者引用计数永远不为 0 ,内存永远不会释放
1 2 3 node1 [refcount=2] --> Node1对象 ↑next ↓prev Node2对象 <-- [refcount=2] node2
解决方案:weak_ptr
1 2 3 4 5 6 7 8 9 10 11 12 class SafeNode {public : std::shared_ptr<SafeNode> next; std::weak_ptr<SafeNode> prev; ~SafeNode () { std::cout << "SafeNode destroyed\n" ; } }; auto node1 = std::make_shared <SafeNode>();auto node2 = std::make_shared <SafeNode>();node1->next = node2; node2->prev = node1;
何时会出现循环引用?
双向链表、树结构等复杂数据结构
对象相互持有对方的 shared_ptr
父子对象互相强引用
观察者模式中主体和观察者互相持有
手撕 shared_ptr
|面试高频场景题 1. 非线程安全的简单实现 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 #include <memory> template <typename T>class smartPtr {private : T *_ptr; size_t * _count; public : smartPtr (T *ptr = nullptr ):_ptr(ptr) { if (_ptr) { _count = new size_t (1 ); } else { _count = new size_t (0 ); } } smartPtr (const smartPtr &ptr) { if (this != &ptr) { this ->_ptr = ptr._ptr; this ->_count = ptr._count; ++(*this ->_count) ; } } smartPtr& operator =(const smartPtr &ptr) { if (this ->_ptr == ptr._ptr) return *this ; if (this ->_ptr) { --(*this ->_count); if (this ->_count == 0 ) { delete this ->_ptr; delete this ->_count; } } this ->_ptr = ptr._ptr; this ->_count = ptr._count; ++(*this ->_count); return *this ; } ~smartPtr () { --(*this ->_count); if (0 == *this ->_count) { delete this ->_ptr; delete this ->_count; } } size_t use_count () { return *this ->_count; } T& operator *() { assert (this ->_ptr == nullptr ); return *(this ->_ptr); } T* operator ->() { assert (this ->_ptr == nullptr ); return this ->_ptr; } };
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 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 #pragma once #include <atomic> template <typename T>class shared_ptr {private : T* ptr; std::atomic<std::size_t >* ref_count; void release () { if (ref_count && ref_count->fetch_sub (1 , std::memory_order_acq_rel) == 1 ) { delete ptr; delete ref_count; } } public : shared_ptr () : ptr (nullptr ), ref_count (nullptr ) {} explicit shared_ptr (T* p) : ptr(p), ref_count(p ? new std::atomic<std::size_t>(1 ) : nullptr) { } ~shared_ptr () { release (); } shared_ptr (const shared_ptr<T>& other) : ptr (other.ptr), ref_count (other.ref_count) { if (ref_count) { ref_count->fetch_add (1 , std::memory_order_relaxed); } } shared_ptr<T>& operator =(const shared_ptr<T>& other) { if (this != &other) { release (); ptr = other.ptr; ref_count = other.ref_count; if (ref_count) { ref_count->fetch_add (1 , std::memory_order_relaxed); } } return *this ; } shared_ptr (shared_ptr<T>&& other) noexcept : ptr (other.ptr), ref_count (other.ref_count) { other.ptr = nullptr ; other.ref_count = nullptr ; } shared_ptr<T>& operator =(shared_ptr<T>&& other) noexcept { if (this != &other) { release (); ptr = other.ptr; ref_count = other.ref_count; other.ptr = nullptr ; other.ref_count = nullptr ; } return *this ; } T& operator *() const { return *ptr; } T* operator ->() const { return ptr; } std::size_t use_count () const { return ref_count ? ref_count->load (std::memory_order_acquire) : 0 ; } T* get () const { return ptr; } void reset (T* p = nullptr ) { release (); ptr = p; ref_count = p ? new std::atomic <std::size_t >(1 ) : nullptr ; } };