
C智能指针完全指南从内存泄漏到RAII前言在C开发中内存管理一直是令程序员头疼的问题。忘记释放内存导致内存泄漏异常发生时资源无法释放多个指针指向同一块内存导致重复释放……这些问题几乎每个C开发者都遇到过。智能指针的出现就是为了从根本上解决这些问题。本文将系统地介绍智能指针的前世今生从RAII思想到C11标准库中的智能指针帮助大家彻底掌握这一现代C核心工具。一、为什么需要智能指针先来看一段代码思考其中存在什么问题intdiv(){inta,b;cinab;if(b0)throwinvalid_argument(除0错误);returna/b;}voidFunc(){int*p1newint;int*p2newint;coutdiv()endl;deletep1;deletep2;}intmain(){try{Func();}catch(exceptione){coute.what()endl;}return0;}这段代码存在三个潜在问题new int抛出异常如果p1申请内存失败p2和后续代码都不会执行Func直接退出但此时没有任何异常被抛出程序会直接崩溃。第二个new抛出异常如果p1申请成功但p2申请失败p1没有被释放造成内存泄漏。div()抛出异常如果p1和p2都申请成功但div()抛出异常delete p1和delete p2不会执行造成内存泄漏。结论传统的new/delete在异常发生时无法保证资源被正确释放。这就是我们需要智能指针的根本原因。二、内存泄漏2.1 什么是内存泄漏内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏不会让内存物理消失而是程序失去了对某段内存的控制权导致内存被白白占用。2.2 内存泄漏的危害对于长期运行的程序如操作系统、后台服务内存泄漏会导致内存占用持续增长程序响应越来越慢最终系统资源耗尽程序卡死2.3 内存泄漏的分类类型说明示例堆内存泄漏通过malloc/new分配的内存未释放int* p new int;忘记delete系统资源泄漏系统分配的资源未释放文件句柄、套接字、管道未关闭2.4 如何避免内存泄漏事前预防使用RAII思想或智能指针管理资源规范编码申请内存后匹配释放但异常发生时仍可能出问题使用工具ValgrindLinux、VLDWindows等内存检测工具内部内存管理库自带泄漏检测功能核心观点最可靠的方案是智能指针它从设计上杜绝了内存泄漏的可能。三、智能指针的原理3.1 RAII思想RAIIResource Acquisition Is Initialization是C中管理资源的核心理念在对象构造时获取资源在对象析构时释放资源。利用对象的生命周期来控制资源将资源管理责任托管给对象。这样做的好处不需要显式释放资源资源在对象生命周期内始终有效即使发生异常析构函数也会被调用资源得以释放templateclassTclassSmartPtr{public:SmartPtr(T*ptrnullptr):_ptr(ptr){}~SmartPtr(){if(_ptr)delete_ptr;}private:T*_ptr;};voidFunc(){SmartPtrintsp1(newint);SmartPtrintsp2(newint);coutdiv()endl;// sp1和sp2析构时自动释放内存}3.2 智能指针的行为智能指针不仅要管理资源还要像普通指针一样使用。需要重载operator*和operator-templateclassTclassSmartPtr{public:SmartPtr(T*ptrnullptr):_ptr(ptr){}~SmartPtr(){if(_ptr)delete_ptr;}Toperator*(){return*_ptr;}T*operator-(){return_ptr;}private:T*_ptr;};structDate{int_year;int_month;int_day;};intmain(){SmartPtrintsp1(newint);*sp110;// 像指针一样解引用SmartPtrDatesp2(newDate);sp2-_year2022;// 像指针一样访问成员// 实际上是 sp2.operator-()-_year 2022}3.3 智能指针的三种演变理解了智能指针的基本原理后我们来看看三种智能指针是如何解决不同问题的。① auto_ptrC98—— 失败的设计auto_ptr采用管理权转移策略namespacebit{templateclassTclassauto_ptr{public:auto_ptr(T*ptr):_ptr(ptr){}// 拷贝构造转移管理权auto_ptr(auto_ptrTsp):_ptr(sp._ptr){sp._ptrnullptr;// 原指针悬空}~auto_ptr(){if(_ptr)delete_ptr;}private:T*_ptr;};}intmain(){auto_ptrintsp1(newint);auto_ptrintsp2(sp1);// 管理权转移sp1悬空*sp210;*sp120;// 崩溃sp1已经是空指针}问题拷贝后原指针变为nullptr如果再使用原指针会导致程序崩溃。这是auto_ptr被业界诟病的原因很多公司明令禁止使用。② unique_ptrC11—— 简单粗暴unique_ptr采用防拷贝策略namespacebit{templateclassTclassunique_ptr{public:unique_ptr(T*ptr):_ptr(ptr){}~unique_ptr(){if(_ptr)delete_ptr;}// 禁止拷贝unique_ptr(constunique_ptrTsp)delete;unique_ptrToperator(constunique_ptrTsp)delete;private:T*_ptr;};}intmain(){unique_ptrintsp1(newint);// unique_ptrint sp2(sp1); // 编译错误拷贝构造被删除}特点简单粗暴从语法层面禁止拷贝保证了资源的唯一所有权。适用于明确的独占场景。③ shared_ptrC11—— 引用计数shared_ptr采用引用计数策略允许多个智能指针共享同一份资源namespacearamae{templateclassTclassshared_ptr{public:shared_ptr(T*ptrnullptr):_ptr(ptr),_pRefCount(newint(1)),_pmtx(newmutex){}// 拷贝构造增加引用计数shared_ptr(constshared_ptrTsp):_ptr(sp._ptr),_pRefCount(sp._pRefCount),_pmtx(sp._pmtx){AddRef();// 引用计数1}// 析构减少引用计数~shared_ptr(){Release();// 引用计数-1若为0则释放}// 赋值操作符shared_ptrToperator(constshared_ptrTsp){if(_ptr!sp._ptr){Release();// 释放当前资源_ptrsp._ptr;_pRefCountsp._pRefCount;_pmtxsp._pmtx;AddRef();// 增加新资源引用计数}return*this;}intuse_count(){return*_pRefCount;}private:voidAddRef(){_pmtx-lock();(*_pRefCount);_pmtx-unlock();}voidRelease(){_pmtx-lock();boolflagfalse;if(--(*_pRefCount)0_ptr){delete_ptr;delete_pRefCount;flagtrue;}_pmtx-unlock();if(flag)delete_pmtx;}T*_ptr;int*_pRefCount;// 引用计数mutex*_pmtx;// 线程安全锁};}引用计数工作原理操作引用计数变化资源状态创建第一个shared_ptr0 → 1资源被持有拷贝构造1多一个持有者析构-1减少一个持有者引用计数变为00 → 释放资源被释放四、shared_ptr的线程安全问题shared_ptr的线程安全需要从两个方面理解4.1 引用计数操作的线程安全 ✅引用计数的和--是线程安全的因为内部使用了互斥锁保护。4.2 所管理资源的线程安全 ❌shared_ptr不保证所管理资源的线程安全。如果有多个线程同时访问同一资源需要外部加锁structDate{int_year0;int_month0;int_day0;};voidThreadFunc(shared_ptrDatesp,size_t n,mutexmtx){for(size_t i0;in;i){shared_ptrDatecopy(sp);// 引用计数线程安全{lock_guardmutexlock(mtx);// 访问资源需要外部加锁copy-_year;copy-_month;copy-_day;}}}intmain(){shared_ptrDatep(newDate);mutex mtx;threadt1(ThreadFunc,ref(p),100000,ref(mtx));threadt2(ThreadFunc,ref(p),100000,ref(mtx));t1.join();t2.join();// 结果一定是 200000}五、shared_ptr的循环引用问题5.1 什么是循环引用structListNode{int_data;shared_ptrListNode_prev;shared_ptrListNode_next;~ListNode(){cout~ListNode()endl;}};intmain(){shared_ptrListNodenode1(newListNode);shared_ptrListNodenode2(newListNode);node1-_nextnode2;// node2引用计数变为2node2-_prevnode1;// node1引用计数变为2// 离开作用域// node1析构引用计数 2→1node2的_prev还指向node1// node2析构引用计数 2→1node1的_next还指向node2// 引用计数永远到不了0内存泄漏}5.2 循环引用的示意图5.3 解决方案weak_ptr将_prev和_next改为weak_ptrstructListNode{int_data;weak_ptrListNode_prev;// 不增加引用计数weak_ptrListNode_next;// 不增加引用计数~ListNode(){cout~ListNode()endl;}};intmain(){shared_ptrListNodenode1(newListNode);shared_ptrListNodenode2(newListNode);node1-_nextnode2;// node2引用计数仍为1weak_ptr不增加node2-_prevnode1;// node1引用计数仍为1weak_ptr不增加// 离开作用域// node1析构引用计数 1→0释放node1// node2析构引用计数 1→0释放node2// 正确释放}5.4 weak_ptr的特点特性说明不控制资源生命周期不增加引用计数可以检测资源是否存在通过expired()或lock()解决循环引用打破shared_ptr的循环依赖六、删除器Deletershared_ptr支持自定义删除器管理new之外的资源// 仿函数删除器templateclassTstructFreeFunc{voidoperator()(T*ptr){coutfree: ptrendl;free(ptr);}};templateclassTstructDeleteArrayFunc{voidoperator()(T*ptr){coutdelete[]: ptrendl;delete[]ptr;}};intmain(){// 管理malloc分配的内存shared_ptrintsp1((int*)malloc(4),FreeFuncint());// 管理数组shared_ptrintsp2(newint[10],DeleteArrayFuncint());// 管理文件shared_ptrFILEsp3(fopen(test.txt,w),[](FILE*p){fclose(p);});// lambda删除器shared_ptrintsp4(newint[10],[](int*p){delete[]p;});}七、C11与Boost中智能指针的关系时间版本智能指针特点C98标准库auto_ptr管理权转移已被废弃Boost第三方库scoped_ptr/shared_ptr/weak_ptr更完善的实现C11标准库unique_ptr/shared_ptr/weak_ptr吸收Boost精华对应关系boost::scoped_ptr→std::unique_ptrboost::shared_ptr→std::shared_ptrboost::weak_ptr→std::weak_ptrC11和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中的实现的。八、智能指针选择指南8.1 选择决策树需要多个指针共享同一资源 ├── 是 → 使用 shared_ptr │ ├── 有循环引用 → 配合 weak_ptr 打破循环 │ └── 无循环引用 → 直接用 shared_ptr │ └── 否 → 使用 unique_ptr ├── 需要转移所有权 → 用 std::move └── 不需要转移 → 保持独占8.2 最佳实践场景推荐方案原因独占资源unique_ptr零开销明确所有权共享资源shared_ptr引用计数管理打破循环引用weak_ptr不增加引用计数工厂函数返回unique_ptr明确转移所有权容器中存储指针unique_ptr或shared_ptr自动管理内存总结智能指针是现代C资源管理的核心工具本文要点总结如下知识点核心内容为什么需要解决异常安全问题和内存泄漏RAII思想对象构造获取资源析构释放资源auto_ptr管理权转移已被废弃unique_ptr防拷贝独占所有权shared_ptr引用计数共享所有权weak_ptr配合shared_ptr打破循环引用线程安全引用计数安全资源访问需外部加锁删除器支持自定义资源释放方式