智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new / 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
| int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; }
void Func() { int* p1 = new int; int* p2 = new int; cout << div() << endl; delete p1; delete p2; }
int main() { try { Func(); } catch (exception& e) { cout << e.what() << endl; } return 0; }
|
如果 p1 这里 new 抛异常会如何?
p1 未成功分配,值为 nullptr
函数直接跳转到 catch 块,p2 未分配,无内存泄漏
如果 p2 这里 new 抛异常会如何?
p1 已分配但未释放,导致内存泄漏
函数跳转到 catch 块,p2 未分配,delete p1 和 delete p2 均未执行
如果 div 调用这里又会抛异常会如何?
p1 和 p2 均已分配但未释放,导致双重内存泄漏
函数跳转到 catch 块,打印错误信息(如 “除 0 错误”)
C++ 不像 java 具有垃圾回收机制,能够自动回收开辟的空间,需要自行手动管理,但是自己管理有时又太麻烦了,况且这里只是两个指针就产生了这么多问题,因此在 C++11 就推出了智能指针用于自动管理内存
智能指针原理
RAll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| template<class T> class SmartPtr { public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {} ~SmartPtr() { if (_ptr) delete _ptr; }
private: T* _ptr; };
int main() { SmartPtr<int> sp1(new int(1)); SmartPtr<string> sp2(new string("xxx")); return 0; }
|
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
简单来说,就是把创建的对象给到 SmartPtr 类来管理,当对象的生命周期结束的时候,刚好类也会自动调用析构函数进行内存释放
这种做法有两大好处:
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
像指针一样使用
都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可
以通过 -> 去访问所指空间中的内容,因此类中还得需要将 * 、-> 重载下,才可让其像指针一样去使用
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
| template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {}
~SmartPtr() { cout << "delete:" << _ptr << endl; delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; } private: T* _ptr; };
|
* 重载返回对象,-> 重载返回地址,这部分的知识点在迭代器底层分析已经讲过很多遍了,就不过多叙述了,可自行翻阅前文
C++11的智能指针
智能指针一般放在 <memery> 文件里,C++11 也参考了第三方库 boost
C++ 98 中产生了第一个智能指针 auto_ptr
C++ boost 给出了更实用的 scoped_ptr 和 shared_ptr 和 weak_ptr
C++ TR1,引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版
C++ 11,引入了 unique_ptr 和 shared_ptr 和 weak_ptr。需要注意的是 unique_ptr对应 boost 的 scoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的
auto_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
| template<class T> class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {}
~auto_ptr() { cout << "delete:" << _ptr << endl; delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; } auto_ptr(auto_ptr<T>& ap) :_ptr(ap._ptr) { ap._ptr = nullptr; }
auto_ptr<T>& operator=(auto_ptr<T>& ap) { if (this != &ap) { _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } private: T* _ptr; };
|
auto_ptr 在 C++98 就已经被引入,实现了智能指针如上面所讲的最基础的功能,同时他还额外对拷贝构造、= 重载进行了显式调用,但是这种拷贝虽然能解决新对象的初始化,但是对于被拷贝的对象,造成了指针资源所有权被转移走,跟移动构造有些类似
因此,auto_ptr 会导致管理权转移,拷贝对象被悬空,auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_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
| template<class T> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) {}
~unique_ptr() { cout << "delete:" << _ptr << endl; delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
unique_ptr(unique_ptr<T>& ap) = delete; unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete; private: T* _ptr; };
|
unique_ptr 很简单粗暴,直接禁止了拷贝机制
因此,建议在不需要拷贝的场景使用该智能指针
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
| template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(ptr) ,_pcount(new int(1)) {}
~shared_ptr() { if (--(*_pcount) == 0) { cout << "delete:" << _ptr << endl; delete _ptr; delete _pcount; } }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) ,_pcount(sp._pcount) { ++(*_pcount); }
shared_ptr<T>& operator=(const shared_ptr<T>& sp) { if (_ptr == sp._ptr) return *this;
if (--(*_pcount) == 0) { delete _ptr; delete _pcount; }
_ptr = sp._ptr; _pcount = sp._pcount; ++(*_pcount);
return *this; }
int use_count() const { return *_pcount; }
T* get() const { return _ptr; }
private: T* _ptr; int* _pcount; };
|
C++11 中的智能指针就属 shared_ptr 使用的最多,因为它解决了赋值造成的资源被转移可能会被错误访问的问题
类中增加一个新的指针 _pcount 用于计数,即计数有多少个 _ptr 指向同一片空间,多个 shared_ptr 可以同时指向同一个对象,每次创建新的 shared_ptr 指向该对象,引用计数加 1;每次 shared_ptr 析构或者被赋值为指向其他对象,引用计数减 1。当最后一个指向该对象的 shared_ptr 析构时,对象会被自动删除,从而避免内存泄漏
🔥值得注意的是: shared_ptr 同时也支持了无法自己给自己赋值,这里还涉及一些关于线程安全的知识点,待 Linux 学习过后再来补充
weak_ptr
看似完美的 shared_ptr 其实也会有疏漏,比如:引用循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct ListNode { int _data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev; }; int main() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> node2(new ListNode); cout << node1.use_count() << endl; cout << node2.use_count() << endl; node1->_next = node2; node2->_prev = node1; cout << node1.use_count() << endl; cout << node2.use_count() << endl; return 0; }
|
当执行 node1->next = node2 和 node2->prev = node1 时,node1 内部的 _next 指针指向 node2 ,node2 内部的 _prev 指针指向 node1 。这就导致两个节点之间形成了循环引用关系。此时,由于互相引用,每个节点的引用计数都变为 2 ,因为除了外部的智能指针引用,还多了来自另一个节点内部指针的引用
当 node1 和 node2 智能指针对象离开作用域开始析构时,它们首先会将所指向节点的引用计数减 1 。此时,每个节点的引用计数变为 1 ,而不是预期的 0 。这是因为 node1 的 _next 还指向 node2 ,node2 的 _prev 还指向 node1 ,使得它们的引用计数无法归零
对于 shared_ptr 来说,只有当引用计数变为 0 时才会释放所管理的资源。由于这种循环引用的存在,node1 等待 node2 先释放(因为 node2 的 _prev 引用着 node1 ),而 node2 又等待 node1 先释放(因为 node1 的 _next 引用着 node2 ),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏
1 2 3 4 5 6
| class ListNode { public: weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev; };
|
为了解决 shared_ptr 的循环引用问题,通常可以使用 weak_ptr 。weak_ptr 是一种弱引用智能指针,它不会增加所指向对象的引用计数。将循环引用中的某一个引用(比如 ListNode 类中的 _prev 或 _next 其中之一)改为 weak_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
| #include <iostream> #include <memory> #include <string>
using namespace std;
class A { public: ~A() { cout << "A::~A()" << endl; } };
template<class T> struct FreeFunc { void operator()(T* ptr) const { cout << "FreeFunc: free memory at " << ptr << endl; free(ptr); } };
template<class T> struct DeleteArrayFunc { void operator()(T* ptr) const { cout << "DeleteArrayFunc: delete[] memory at " << ptr << endl; delete[] ptr; } };
int main() { shared_ptr<int> sp1((int*)malloc(sizeof(int)), FreeFunc<int>()); *sp1 = 100; cout << "sp1: " << *sp1 << " at " << sp1.get() << endl;
shared_ptr<int> sp2(new int[5], DeleteArrayFunc<int>()); for (int i = 0; i < 5; ++i) { sp2.get()[i] = i; } cout << "sp2 array:"; for (int i = 0; i < 5; ++i) { cout << " " << sp2.get()[i]; } cout << endl;
shared_ptr<A> sp4(new A[3], [](A* p) { cout << "Lambda: deleting array at " << p << endl; delete[] p; }); cout << "sp4 array of A objects created" << endl;
shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) { if (p) { cout << "Lambda: closing file" << endl; fclose(p); } }); if (sp5) { fprintf(sp5.get(), "Hello, shared_ptr with deleter!\n"); cout << "File written" << endl; }
return 0; }
|
对于所有的指针不一定是 new 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!
