ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++ 智能指针:解决异常引起的内存泄漏问题

2026/9/16 8:37:58 拓冰建站 浏览量
C++ 智能指针:解决异常引起的内存泄漏问题 目录一. 智能指针使用场景的分析二. RAII和智能指针的设计思路2.1 智能指针代码示例2.2 代码示例三. C标准库的智能指针的使用四. 智能指针的原理4.1 auto_ptr 的模拟实现4.2 unique_ptr 的模拟实现4.3 shared_ptr 的模拟实现五. shared_ptr 和 weak_ptr5.1 shared_ptr 的循环问题5.2 weak_ptr5.2.1 weak_ptr 的原理5.2.2 weak_ptr 的模拟实现六. C11和boost中智能指针的关系七. 内存泄漏7.1 什么是内存泄漏内存泄漏的危害7.2 如何检测内存泄漏了解7.3 如何避免内存泄漏一. 智能指针使用场景的分析下面程序中我们可以看到new了以后我们也delete了但是因为抛异常后面的delete没有得到执行所以就内存泄漏了所以我们需要new以后捕获异常捕获到异常后delete内存再把异常抛出但是因为new本身也可能抛异常连续的两个new和下面的Divide都可能会抛异常让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。double Divide(int a, int b) { // 当b 0时抛出异常 if (b 0) { throw Divide by zero condition!; } else { return (double)a / (double)b; } } void Func() { // 这⾥可以看到如果发⽣除0错误抛出异常另外下⾯的array和array2没有得到释放。 // 所以这⾥捕获异常后并不处理异常异常还是交给外⾯处理这⾥捕获了再重新抛出去。 // 但是如果array2new的时候抛异常呢就还需要套⼀层捕获释放逻辑这⾥更好解决⽅案 // 是智能指针否则代码太戳了 int* array1 new int[10]; int* array2 new int[10]; // 抛异常呢 try { int len, time; cin len time; cout Divide(len, time) endl; } catch (...) { cout delete [] array1 endl; cout delete [] array2 endl; delete[] array1; delete[] array2; throw; // 异常重新抛出捕获到什么抛出什么 } // ... cout delete [] array1 endl; delete[] array1; cout delete [] array2 endl; delete[] array2; } int main() { try { Func(); } catch (const char* errmsg) { cout errmsg endl; } catch (const exception e) { cout e.what() endl; } catch (...) { cout 未知异常 endl; } return 0; }二. RAII和智能指针的设计思路• RAII是ResourceAcquisition Is Initialization的缩写他是一种管理资源的类的设计思想本质是一种利用对象生命周期来管理获取到的动态资源避免资源泄漏这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个对象接着控制对资源的访问资源在对象的生命周期内始终保持有效最后在对象析构的时候释放资源这样保障了资源的正常释放避免资源泄漏问题。• 智能指针类除了满足RAII的设计思路还要方便资源的访问所以智能指针类还会想迭代器类一样重载operator*/operator-/operator[]等运算符方便访问资源。2.1 智能指针代码示例templateclass T class SmartPtr { public: // RAII SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout delete[] _ptr endl; delete[] _ptr; } // 重载运算符模拟指针的⾏为⽅便访问资源 T operator*() { return *_ptr; } T* operator-() { return _ptr; } T operator[](size_t i) { return _ptr[i]; } private: T* _ptr; };2.2 代码示例templateclass T class SmartPtr { public: // RAII SmartPtr(T* ptr) :_ptr(ptr) { } ~SmartPtr() { cout delete[] _ptr endl; delete[] _ptr; } // 重载运算符模拟指针的⾏为⽅便访问资源 T operator*() { return *_ptr; } T* operator-() { return _ptr; } T operator[](size_t i) { return _ptr[i]; } private: T* _ptr; }; double Divide(int a, int b) { // 当b 0时抛出异常 if (b 0) { throw Divide by zero condition!; } else { return (double)a / (double)b; } } void Func() { // 这⾥使⽤RAII的智能指针类管理new出来的数组以后程序简单多了 SmartPtrint sp1 new int[10]; SmartPtrint sp2 new int[10]; for (size_t i 0; i 10; i) { sp1[i] sp2[i] i; } int len, time; cin len time; cout Divide(len, time) endl; } int main() { try { Func(); } catch (const char* errmsg) { cout errmsg endl; } catch (const exception e) { cout e.what() endl; } catch (...) { cout 未知异常 endl; } return 0; }三. C标准库的智能指针的使用• C标准库中的智能指针都在这个momery头文件下面我们包含就可以使用了智能指针有好几种除了weak_ptr他们都符合RAII和像指针一样访问的行为原理上而言主要是解决智能指针拷贝时的思路不同。•auto_ptr是C98时设计出来的智能指针他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象这是一个非常糟糕的设计因为他会导致被拷贝对象悬空访问报错的问题C11设计出新的智能指针后强烈建议不要使用auto_ptr。其他C11出来之前很多公司也是明令禁止使用这个智能指针的。struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) { } ~Date() { cout ~Date() endl; } }; int main() { auto_ptrDate ap1(new Date); // 拷贝时管理权限转移被拷贝对象ap1悬空 auto_ptrDate ap2(ap1); // 空指针访问ap1对象已经悬空 //ap1-_year; return 0; }•unique_ptr是C11设计出来的智能指针他的名字翻译出来是唯一指针他的特点的不支持拷贝只支持移动。如果不需要拷贝的场景就非常建议使用他。struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) { } ~Date() { cout ~Date() endl; } }; int main() { unique_ptrDate up1(new Date); // 不支持拷贝 //unique_ptrDate up2(up1); // 支持移动但是移动后up1也悬空所以使用移动要谨慎 unique_ptrDate up3(move(up1)); return 0; }•shared_ptr是C11设计出来的智能指针他的名字翻译出来是共享指针他的特点是支持拷贝也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) { } ~Date() { cout ~Date() endl; } }; int main() { shared_ptrDate sp1(new Date); shared_ptrDate sp5(new Date); // ⽀持拷⻉ shared_ptrDate sp2(sp1); shared_ptrDate sp3(sp2); cout sp1.use_count() endl; sp1-_year; cout sp1-_year endl; cout sp2-_year endl; cout sp3-_year endl; // ⽀持移动但是移动后sp1也悬空所以使⽤移动要谨慎 shared_ptrDate sp4(move(sp1)); sp4 move(sp5); return 0; }• weak_ptr是C11设计出来的智能指针他的名字翻译出来是弱指针他完全不同于上面的智能指针他不支持RAII也就意味着不能用它直接管理资源weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题。具体细节下面我们再细讲。• 智能指针析构时默认是进行delete释放资源这也就意味着如果不是new出来的资源交给智能指针管理析构时就会崩溃。智能指针支持在构造时给一个删除器所谓删除器本质就是一个可调用对象这个可调用对象中实现你想要的释放资源的方式当构造智能指针时给了定制的删除器在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用所以为了简洁一点unique_ptr和shared_ptr都特化了一份[]的版本使用时unique_ptr up1(new Date[5]);shared_ptr sp1(new Date[5]); 就可以管理new[]的资源。// 这样实现程序会崩溃 // unique_ptrDate up1(new Date[10]); // shared_ptrDate sp1(new Date[10]); // 解决方案1 // 因为new[]经常使用所以unique_ptr和shared_ptr // 实现了⼀个特化版本这个特化版本析构时用的delete[] unique_ptr Date[] up1(new Date[5]); shared_ptr Date[] sp1(new Date[5]);templateclass T class DeleteArray { public: void operator()(T* _ptr) { delete[] _ptr; } }; templateclass T void Delete(T* _ptr) { delete[] _ptr; } int main() { //lambda 函数 std::shared_ptrint dict(new int[6], [](int* _ptr) {delete[] _ptr; }); //仿函数 std::shared_ptrint dict(new int[6], DeleteArrayint()); //普通函数 std::shared_ptrint dict(new int[6], Deleteint); //仿函数 std::unique_ptrint, DeleteArrayint dict(new int[5]); return 0; }• template shared_ptr make_shared (Args... args);shared_ptr除了支持用指向资源的指针构造还支持make_shared用初始化资源对象的值直接构造。std::shared_ptrint dict(new int(1)); std::shared_ptrint dict make_sharedint(2);• shared_ptr 和 unique_ptr 都支持了operator bool的类型转换如果智能指针对象是一个空对象没有管理资源则返回false否则返回true意味着我们可以直接把智能指针对象给if判断是否为空。std::shared_ptrint sp1(new int(1)); std::shared_ptrint sp2; if (sp1) cout sp1 is not nullptr endl; if(!sp2) cout sp2 is nullptr endl;• shared_ptr 和 unique_ptr 都得构造函数都使用explicit修饰防止普通指针隐式类型转换成智能指针对象。//不能这样写 std::shared_ptrint sp1 new int(1);四. 智能指针的原理4.1 auto_ptr 的模拟实现templateclass T class auto_ptr { public: T operator*() { return *_ptr; } T operator[](size_t pos) { return _ptr[pos]; } T* operator-() { return _ptr; } auto_ptr(T* ptr) :_ptr(ptr) {} //管理权转移 auto_ptr(auto_ptrT pt) :_ptr(pt._ptr) { pt._ptr nullptr; } auto_ptrT operator(auto_ptrT pt) { if (this ! pt) { if(_ptr) { delete _ptr } _ptr pt._ptr; pt._ptr nullptr; } return *this; } ~auto_ptr() { if (_ptr) { delete _ptr; } } private: T* _ptr; };4.2 unique_ptr 的模拟实现templateclass T class unique_ptr { public: T operator*() { return *_ptr; } T operator[](size_t pos) { return _ptr[pos]; } T* operator-() { return _ptr; } unique_ptr(T* ptr) :_ptr(ptr) {} unique_ptr(unique_ptrT pt) :_ptr(pt._ptr) { pt._ptr nullptr; } unique_ptrT operator(unique_ptrT pt) { if (this ! pt) { if(_ptr) { delete _ptr } _ptr pt._ptr; pt._ptr nullptr; } return *this; } unique_ptr(const unique_ptrT pt) delete; unique_ptrT operator(const unique_ptrT pt) delete; ~unique_ptr() { if (_ptr) { delete _ptr; } } private: T* _ptr; };4.3 shared_ptr 的模拟实现templateclass T class shared_ptr { public: T operator*() { return *_ptr; } T* operator-() { return _ptr; } T operator[](size_t pos) { return _ptr[pos]; } shared_ptr(T* ptr nullptr) :_ptr(ptr) ,_count(new int(1)) {} ~shared_ptr() { (*_count)--; if (*_count 0) { delete _ptr; delete _count; } _ptr nullptr; _count nullptr; } shared_ptr(const shared_ptrT pt) :_ptr(pt._ptr) ,_count(pt._count) { (*_count); } shared_ptr(shared_ptrT pt) :_ptr(pt._ptr) , _count(pt._count) { pt._count new int(1); pt._ptr nullptr; } shared_ptrT operator(shared_ptrT pt) { if (_ptr ! pt._ptr) { (*_count)--; if (*_count 0) { delete _ptr; delete _count; } _ptr pt._ptr; _count pt._count; pt._count new int(1); pt._ptr nullptr; } return *this; } shared_ptrT operator(const shared_ptrT pt) { if (_ptr ! pt._ptr) { (*_count)--; if (*_count 0) { delete _ptr; delete _count; } _ptr pt._ptr; _count pt._count; (*_count); } return *this; } private: T* _ptr; int* _count; };五. shared_ptr 和 weak_ptr5.1 shared_ptr 的循环问题• shared_ptr大多数情况下管理资源非常合适支持RAII也支持拷贝。但是在循环引用的场景下会导致资源没有得到释放内存泄漏所以我们要认识循环引用的场景和资源没释放的原因并且学会使用weak_ptr解决这种问题。• 如下图所述场景n1和n2析构后管理两个节点的引用计数减到1右边的节点什么时候释放呢左边节点中的_next管着呢_next析构后右边的节点就释放了。_next什么时候析构呢_next是左边节点的的成员左边节点释放_next就析构了。左边节点什么时候释放呢左边节点由右边节点中的_prev管着呢_prev析构后左边的节点就释放了。_prev什么时候析构呢_prev是右边节点的成员右边节点释放_prev就析构了。• 至此逻辑上成功形成回旋镖似的循环引用谁都不会释放就形成了循环引用导致内存泄漏• 把ListNode结构体中的_next和_prev改成weak_ptrweak_ptr绑定到shared_ptr时不会增加它的引用计数_next和_prev不会参与资源释放管理逻辑就成功打破了循环引用解决了这里的问题struct ListNode { int _data; shared_ptrListNode _next; // 互相引用 shared_ptrListNode _prev; ~ListNode() { cout ~ListNode() endl; } }; int main() { shared_ptrListNode n1(new ListNode); shared_ptrListNode n2(new ListNode); cout n1.use_count() , n2.use_count() endl; // 输出1,1 n1-_next n2; // n2计数1 → 2 n2-_prev n1; // n1计数1 → 2 // 析构n1和n2计数各减1 → 1无法释放资源循环引用 cout n1.use_count() endl; cout n2.use_count() endl; return 0; }5.2 weak_ptr5.2.1 weak_ptr 的原理• weak_ptr不支持RAII也不支持访问资源所以我们看文档发现weak_ptr构造时不支持绑定到资源只支持绑定到shared_ptr绑定到shared_ptr时不增加shared_ptr的引用计数那么就可以解决上述的循环引用问题。• weak_ptr也没有重载operator*和operator-等因为他不参与资源管理那么如果他绑定的shared_ptr已经释放了资源那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期use_count也可获取shared_ptr的引用计数weak_ptr想访问资源时可以调用lock返回一个管理资源的shared_ptr如果资源已经被释放返回的shared_ptr是一个空对象如果资源没有释放则通过返回的shared_ptr访问资源是安全的。int main() { std::shared_ptrstring sp1(new string(111111)); std::shared_ptrstring sp2(sp1); std::weak_ptrstring wp sp1; cout wp.expired() endl; cout wp.use_count() endl; // sp1和sp2都指向了其他资源则weak_ptr就过期了 sp1 make_sharedstring(222222); cout wp.expired() endl; cout wp.use_count() endl; sp2 make_sharedstring(333333); cout wp.expired() endl; cout wp.use_count() endl; wp sp1; //std::shared_ptrstring sp3 wp.lock(); auto sp3 wp.lock(); cout wp.expired() endl; cout wp.use_count() endl; *sp3 ###; cout *sp1 endl; return 0; }5.2.2 weak_ptr 的模拟实现templateclass T class weak_ptr { public: weak_ptr() { } weak_ptr(const shared_ptrT sp) :_ptr(sp.get()) { } // 不参与资源的管理 private: T* _ptr nullptr; // int* _pcount; };六. C11和boost中智能指针的关系• Boost库是为C语言标准库提供扩展的一些C程序库的总称Boost社区建立的初衷之一就是为C的标准化工作提供可供参考的实现Boost社区的发起人Dawes本人就是C标准委员会的成员之一。在Boost库的开发中Boost社区也在这个方向上取得了丰硕的成果C11及之后的新语法和库有很多都是从Boost中来的。• C98中产生了第一个智能指针auto_ptr。• C Boost给出了更实用的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等。• C TR1引入了shared_ptr等不过注意的是TR1并不是标准版。• C11引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应Boost的scoped_ptr。并且这些智能指针的实现原理是参考Boost中的实现的。七. 内存泄漏7.1 什么是内存泄漏内存泄漏的危害什么是内存泄漏内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失而是应用程序分配某段内存后因为设计错误失去了对该段内存的控制因而造成了内存的浪费。内存泄漏的危害普通程序运行一会就结束了出现内存泄漏问题也不大进程正常结束页表的映射关系解除物理内存也可以释放。长期运行的程序出现内存泄漏影响很大如操作系统、后台服务、长时间运行的客户端等等不断出现内存泄漏会导致可用内存不断变少各种功能响应越来越慢最终卡死。int main() { // 申请⼀个1G未释放这个程序多次运⾏也没啥危害 // 因为程序⻢上就结束进程结束各种资源也就回收了 char* ptr new char[1024 * 1024 * 1024]; cout (void*)ptr endl; return 0; }7.2 如何检测内存泄漏了解• Linux下几款C程序中的内存泄露检查工具_c内存泄露工具分析-CSDN博客• windows下使用第三方工具windows下的内存泄露检测工具VLD使用7.3 如何避免内存泄漏• 工程前期良好的设计规范养成良好的编码规范申请的内存空间记着匹配地去释放。ps这个理想状态。但是如果碰上异常时就算注意释放了还是可能会出问题。需要下一条智能指针来管理才有保证。• 尽量使用智能指针来管理资源如果自己场景比较特殊采用RAII思想自己造个轮子管理。• 定期使用内存泄漏工具检测尤其是每次项目快上线前不过有些工具不够靠谱或者是收费。• 总结一下内存泄漏非常常见解决方案分为两种1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。