【C++】智能指针(auto/unique/shared_ptr)
目录为什么要使用智能指针智能指针的原理std::auto_ptrC boost 库std::unique_ptrstd::shared_ptrshared_ptr 的原理shared_ptr的循环引用使用 make_shared 创建智能指针智能指针总结定制删除器为什么要使用智能指针int main() { pairstring, string* p1 new pairstring, string; delete p1; cout delete: p1 endl; return 0; }上面代码完全没有内存泄漏的问题但如果在 delete p1 之前调用了其他函数而这个函数抛出了异常导致程序的执行流没有执行到 delete p1 语句就造成了内存泄漏如果这是一个长期运行的程序。必须在捕获异常的同时执行 delete p1 语句void f() { pairstring, string* p1 new pairstring, string; try { func(); } catch (...) { delete p1; cout delete: p1 endl; throw; } delete p1; cout delete: p1 endl; } int main() { try { f(); } catch (const exception e) { cout e.what() endl; } return 0; }如果我多定义几个 p 指针void f() { pairstring, string* p1 new pairstring, string; pairstring, string* p2 new pairstring, string; pairstring, string* p3 new pairstring, string; pairstring, string* p4 new pairstring, string; //...new 可能抛异常即使概率很低如果 p1 抛异常那什么也不用释放如果 p2 抛异常要释放 p1如果 p3 抛异常要释放 p1 和 p2......不仅是 new 可能抛异常func 函数也可能抛异常如果这些异常都要处理这使得代码变得非常冗杂。如何解决这个问题呢可以使用智能指针智能指针的原理RAIIResource Acquisition Is Initialization是一种利用对象生命周期来控制程序资源如内存、文件句柄、网络连接、互斥量等等的简单技术。在对象构造时获取资源接着控制对资源的访问使之在对象的生命周期内始终保持有效最后在对象析构的时候释放资源。借此我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处不需要显式地释放资源。采用这种方式对象所需的资源在其生命期内始终保持有效智能指针其实就是把一个指针交给一个类对象进行管理当退出某个作用域不管是正常退出还是抛异常退出都会调用对象的析构函数释放空间。int div() { int a, b; cin a b; if (b 0) throw invalid_argument(除0错误); return a / b; } templateclass 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; }; void f() { SmartPtrpairstring, string sp1(new pairstring, string(1111, 22222)); //div(); SmartPtrpairstring, string sp2(new pairstring, string); SmartPtrpairstring, string sp3(new pairstring, string); SmartPtrstring sp4(new string(xxxxx)); cout *sp4 endl; cout sp1-first endl; cout sp1-second endl; div(); } int main() { try { f(); } catch (const exception e) { cout e.what() endl; } return 0; }有了智能指针对象在退出某个作用域时不管是正常退出还是抛异常退出自动调用析构函数释放资源再也不用手动 delete也不怕忘记 delete 了。总结一下智能指针的原理1. RAII特性2. 重载 operator* 和 opertaor-具有像指针一样的行为。std::auto_ptr上面的指针还远远不能解决一些问题比如SmartPtrstring sp1(new string(xxxx)); SmartPtrstring sp2(new string(yyyy)); sp1 sp2;sp1 和 sp2 都指向 “xxxx”会导致重复析构的问题。为了解决这个问题auto_ptr 采用管理权转移的思想。即把资源交给拷贝对象管理自己变成空指针。比如上面的 sp1 sp2; 就是把 “yyyy” 的管理权交给 sp1 管理sp1 释放xxxx不再管理 xxxxsp2 置为空。由于auto_ptr 会导致被拷贝对象悬空问题所以一般不会使用 auto_ptr。面试时不要手撕 auto_ptrauto_ptr 的拷贝构造和赋值重载auto_ptr(auto_ptrT sp) :_ptr(sp._ptr) { // 管理权转移 sp._ptr nullptr; } auto_ptrT operator(auto_ptrT ap) { // 检测是否为自己给自己赋值 if (this ! ap) { // 释放当前对象中资源 if (_ptr) delete _ptr; // 转移ap中资源到当前对象中 _ptr ap._ptr; ap._ptr NULL; } return *this; }C boost 库C boost 库简介Boost是由C标准委员会库工作组成员发起的跨平台开源C程序库集合采用Boost License授权协议为C标准库提供扩展功能。其部分组件已被纳入C技术规范TR1和TR2。Boost社区通过提供标准化参考实现推动C语言演进开发成果多次被C标准提案采纳。需注意的是Boost包含部分实验性质的组件实际开发中需谨慎评估适用性。C11出来之前boost库已经实现了更好用的 scoped_ptr / shared_ptr / weak_ptrC11将boost库中智能指针精华部分吸收了过来std::unique_ptrC11中开始提供更靠谱的unique_ptr它的实现原理简单粗暴的不让拷贝。unique_ptr(const unique_ptrT sp) delete; unique_ptrT operator(const unique_ptrT sp) delete;不需要拷贝的场景可以使用 unique_ptr 但有一些需要拷贝的地方可以使用 shared_ptrstd::shared_ptrshared_ptr 的原理auto_ptr 的问题是可能导致重复析构unique_ptr 的解决方法是不让拷贝而 shared_ptr 的解决方法是引用计数。C11中开始提供更靠谱的并且支持拷贝的 shared_ptrshared_ptr的原理是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源。老师晚自习在下课之前都会通知让最后走的学生记得把灯关了。1. shared_ptr在其内部给每个资源都维护一份计数用来记录该份资源被几个对象共享。2. 在对象被销毁时(也就是析构函数调用)说明自己不使用该资源对象的引用计数减1。3. 如果引用计数是0就说明自己是最后一个使用该资源的对象必须释放该资源4. 如果不是0就说明除了自己还有其他对象在使用该份资源不能释放该资源。如何实现引用计数记录该份资源被几个对象共同管理类的数据成员再增加一个静态变量可不可以呢不可以比如下面的场景解决方法是在类中增加一个指向引用计数的指针让这个指针指向它管理的资源的引用计数模拟实现templateclass 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_ptrT sp) :_ptr(sp._ptr) , _pcount(sp._pcount) { (*_pcount); } shared_ptrT operator(const shared_ptrT 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; };注意根据上面的模拟实现我们可以发现 shared_ptr 的正确使用方式应该是int main() { int* p new int(1); shared_ptrint p1(p); cout p1.use_count() endl; // 输出 1 shared_ptrint p2(p1); cout p1.use_count() endl; // 输出 2 cout p2.use_count() endl; // 输出 2 return 0; }即首次使用 shared_ptr 可以使用 shared_ptr 的 shared_ptr(T* ptr nullptr) 构造函数构造往后再想使用 shared_ptr 指向同一份资源就要用 shared_ptr 的 shared_ptr(const shared_ptrT sp) 构造函数构造。如果仍然使用 shared_ptr(T* ptr nullptr) 构造函数构造会导致指向的资源重复析构。shared_ptr 的循环引用shared_ptr 在绝大多数情况下是安全的但在某些场景可能还是会造成内存泄漏struct ListNode { int _data; shared_ptrListNode _prev; shared_ptrListNode _next; ~ListNode() { cout ~ListNode() endl; } }; int main() { shared_ptrListNode node1(new ListNode); shared_ptrListNode node2(new ListNode); node1-_next node2; node2-_prev node1; return 0; }在退出 mian 函数调用析构函数时node1 和 node2 的引用计数为1虽然调用了析构函数但它们没有被释放而它们现在确实应该要被释放。并且出现了逻辑死循环这种现象就叫“循环引用”解决方案在引用计数的场景下把节点中的 _prev 和 _next 改成weak_ptr就可以了原理就是node1-_next node2;和node2-_prev node1;时 weak_ptr 的_next 和 _prev 不会增加 node1 和 node2 的引用计数。struct ListNode { int _data; weak_ptrListNode _prev; weak_ptrListNode _next; ~ListNode(){ cout ~ListNode() endl; } };weak_ptr 不是 RAII 的智能指针是专门用来解决循环引用的指针模拟实现templateclass T class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} weak_ptr(const shared_ptrT sp) :_ptr(sp.get()) {} weak_ptrT operator(const shared_ptrT sp) { _ptr sp.get(); return *this; } T operator*() { return *_ptr; } T* operator-() { return _ptr; } private: T* _ptr; };使用 make_shared 创建智能指针make_shared是 C11 引入的一个函数模板用于安全、高效地创建shared_ptr。基本用法// 使用 make_shared auto p1 std::make_sharedint(42); // 等价于传统写法 std::shared_ptrint p2(new int(42));优势1. 更高性能一次内存分配// make_shared一次分配 auto p std::make_sharedWidget(args...); // 控制块和对象在连续内存块中 // 传统方式两次分配 std::shared_ptrWidget p(new Widget(args...)); // 1. new Widget 分配对象内存 // 2. shared_ptr 构造函数分配控制块内存2. 代码更简洁// 清晰简洁 auto ptr std::make_sharedstd::vectorint(10, 5); // 传统方式较冗长 std::shared_ptrstd::vectorint ptr(new std::vectorint(10, 5));何时不使用 make_shared需要自定义删除器内存极度受限需要精细控制如大对象 少量 weak_ptr使用不完整类型某些场景智能指针总结auto_ptr 管理权转移,会导致被拷贝对象悬空,建议不要使用它unique_ptr 禁止拷贝,简单粗暴。日常使用,不需要拷贝的场景,建议使用它shared_ptr 引用计数支持拷贝,需要拷贝的场景,就使用它。但是要小心构成循环引用,循环引用会导致内存泄漏weak_ptr专门解决 shared_ptr 的循环引用问题定制删除器上面的 shared_ptr 还有一个问题没有解决如果用 new string[10] 来初始化 shared_ptrshared_ptr 的析构函数是 delete 而不是 delete[ ] 会导致未定义行为和内存泄漏。如果用 (int*)malloc(sizeof(int)) 初始化 shared_ptrmalloc 必须与 free 匹配。又比如 fopen 要和 fclose 匹配。定制删除器的使用方法使用仿函数或 lambda 表达式控制资源如何释放templateclass T struct DeleteArray { void operator()(T* ptr) { delete[] ptr; } }; //定制删除器 int main() { shared_ptrA sp1(new A[10], DeleteArrayA()); shared_ptrA sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); }); shared_ptrFILE sp3(fopen(Test.cpp, r), [](FILE* ptr) {fclose(ptr);}); return 0; }定制删除器的模拟实现观察库里的 shared_ptr 的模板参数和它的构造函数的模板参数template class T class shared_ptr;template class U, class D shared_ptr (U* p, D del);D del 就是定制删除器但它只作为构造函数的参数而不是定义 shared_ptr 时的模板参数那么问题来了怎么在析构函数拿到定制删除器呢解决方法就是在 shared_ptr 类中定义一个包装器functionvoid(T*) _del在构造函数初始化它。因为不管是那种析构方式析构函数的返回值一定是 void参数一定是一个指针。对于简单的 new 与 delete 匹配在构造 shared_ptr 时如果不想给定制删除器参数可以给包装器一个缺省值。templateclass D shared_ptr(T* ptr, D del) :_ptr(ptr) , _pcount(new int(1)) , _del(del) {} ~shared_ptr() { if (--(*_pcount) 0) { cout delete: _ptr endl; //delete _ptr; _del(_ptr); delete _pcount; } } functionvoid(T*) _del [](T* ptr) {delete ptr; };