
目录1 裸指针痛点异常场景内存泄漏2 RAII 资源获取即初始化3 C 标准库四种智能指针总览4 auto_ptrC98 旧智能指针为什么被废弃5 unique_ptr独占式智能指针5.1 核心特性5.2 管理数组unique_ptr 特化版本5.3 explicit 构造、operator bool6 shared_ptr共享所有权引用计数机制6.1 引用计数为什么必须存堆上不能用静态成员6.2 make_shared 原理与优势6.3 operator bool7 删除器 Deleter 深度讲解【面试高频坑点】8 weak_ptr 弱指针解决 shared_ptr 循环引用8.1 循环引用完整复现与泄漏成因8.2 weak_ptr 核心特性9 shared_ptr 线程安全细节【面试高频】10 简易模拟实现课件源码仅理解原理不等于标准库完整实现11 内存泄漏完整讲解什么是内存泄漏危害内存泄漏产生的典型场景检测工具12 智能指针发展历史13 选型最佳实践与避坑清单14 面试高频问答Q1RAII 是什么Q2auto_ptr 为什么废弃Q3unique_ptr 为什么要删除拷贝构造Q4shared_ptr 引用计数为什么不能用 static 静态成员Q5shared_ptr 循环引用泄漏原理怎么解决Q6weak_ptr 为什么不能直接访问资源lock () 作用是什么Q7shared_ptr 哪些部分线程安全Q8unique_ptr 和 shared_ptr 删除器的语法区别Q9make_shared 优势是什么Q10explicit 对智能指针起到什么作用1 裸指针痛点异常场景内存泄漏课件核心场景函数内连续多次 new函数中间抛出异常直接跳过后面 delete造成内存泄漏。 手动 try‑catch 处理会变得极度繁琐不仅业务代码要套 try如果其中某一步 new 本身抛出 bad_alloc 异常前面已经 new 出来的内存同样要释放需要多层嵌套捕获。#includeiostream #includeexception using namespace std; double Divide(int a, int b) { if (b 0) { throw Divide by zero condition!; } return static_castdouble(a) / b; } void Func() { int* array1 new int[10]; int* array2 new int[10]; // 这一步new也可能抛bad_alloc int len, time; cin len time; cout Divide(len, time) endl; // 如果Divide抛出异常下面两行永远不会执行内存泄漏 delete[] array1; 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; }如果不用智能指针想要安全释放需要写如下臃肿代码void Func() { int* array1 new int[10]; int* array2 nullptr; try { array2 new int[10]; int len, time; cin len time; cout Divide(len, time) endl; } catch (...) { delete[] array1; delete[] array2; throw; } delete[] array1; delete[] array2; }每多一个 new就要增加分支处理业务逻辑被资源释放逻辑严重污染。根本问题new/delete是手动管理函数任意提前退出return /break/throw 异常都有可能跳过 delete。2 RAII 资源获取即初始化RAIIResource Acquisition Is Initialization资源获取即初始化核心四条原则获取资源在构造函数完成资源申请new、fopen、pthread_mutex_init资源托管给栈对象资源交由栈上对象管理对象生命周期内资源保持有效可以正常访问释放资源在析构函数完成资源释放。栈对象关键特性无论函数正常 return、还是异常栈展开 (stack‑unwind) 退出栈对象一定会调用析构函数资源一定会释放。智能指针就是 RAII 范式的典型实例除了析构释放资源还重载*、-、[]模仿原生指针的使用语法。简易 RAII 原型仅演示原理不考虑拷贝问题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; } // 数组下标访问 T operator[](size_t i) { return _ptr[i]; } private: T* _ptr; };使用 RAII 改造上面泄漏示例不需要手写任何 deletevoid Func() { SmartPtrint sp1 new int[10]; SmartPtrint sp2 new int[10]; int len, time; cin len time; cout Divide(len, time) endl; // 无论是否抛异常sp1 sp2出作用域析构自动释放内存 }3 C 标准库四种智能指针总览头文件memory表格智能指针标准版本所有权语义拷贝 / 移动行为底层实现使用场景auto_ptrC98独占拷贝时转移管理权源对象置空裸指针❌C11 起废弃禁止使用unique_ptrC11独占所有权删除拷贝构造 / 拷贝赋值只支持 std::move 移动裸指针一份资源只归一个对象持有优先使用shared_ptrC11共享所有权支持拷贝、移动裸指针 堆上分配引用计数控制块多个对象共同持有同一份资源weak_ptrC11弱引用不拥有资源仅可以从 shared_ptr 构造 / 赋值仅保存资源裸指针不修改计数解决 shared_ptr 循环引用不执行资源释放公共通用特性构造函数使用explicit修饰禁止裸指针隐式转换为智能指针对象规避隐蔽 bug重载operator bool支持if(sp)直接判断智能指针是否持有有效资源默认释放行为调用普通delete支持自定义删除器处理数组、文件句柄、锁等其他资源。4 auto_ptrC98 旧智能指针为什么被废弃auto_ptr 的拷贝语义发生拷贝构造 / 拷贝赋值时把资源管理权转移给目标对象自身内部指针置为 nullptr。模拟核心行为templateclass T class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {} // 拷贝构造转移管理权源对象置空 auto_ptr(auto_ptrT sp) { _ptr sp._ptr; sp._ptr nullptr; } auto_ptrT operator(auto_ptrT ap) { if(this ! ap) { delete _ptr; _ptr ap._ptr; ap._ptr nullptr; } return *this; } ~auto_ptr() { delete _ptr; } T operator*(){return *_ptr;} T* operator-(){return _ptr;} private: T* _ptr; };致命缺陷auto_ptrint p1(new int(10)); auto_ptrint p2(p1); // p1的资源转移给p2p1变成空指针 // *p1; // 运行时崩溃p1已经悬空一旦发生拷贝源对象直接悬空而拷贝会发生在很多隐式场景函数传参、函数返回值极易触发空指针访问因此 C11 直接废弃auto_ptr工程严禁使用。5 unique_ptr独占式智能指针5.1 核心特性独占语义同一时刻一份资源只能被一个 unique_ptr 持有使用 delete显式删除拷贝构造函数、拷贝赋值运算符编译期直接禁止拷贝仅支持移动构造unique_ptr(unique_ptr)、移动赋值operator(unique_ptr)通过std::move转移资源所有权move 之后源对象内部指针置空离开作用域析构执行 delete 释放资源。#includememory #includeiostream using namespace std; struct Date { int _year; int _month; int _day; Date(int y1,int m1,int d1):_year(y),_month(m),_day(d){} ~Date(){cout~Date()endl;} }; int main() { unique_ptrDate up1(new Date(2025,1,1)); // unique_ptrDate up2(up1); // 编译报错拷贝被delete禁止 unique_ptrDate up3(move(up1)); // move转移所有权 cout (up1 ? up1有效 : up1为空) endl; // up1已经被置空 cout up3-_year endl; return 0; }5.2 管理数组unique_ptrT[]特化版本普通unique_ptrT析构调用delete不能释放数组标准库提供模板特化unique_ptrT[]析构调用delete[]。unique_ptrDate[] arr(new Date[5]); arr[0]._year 2025;5.3 explicit 构造、operator boolexplicit unique_ptr(T* p)不允许裸指针隐式转换// unique_ptrDate up new Date(); //编译报错explicit禁止隐式转换 unique_ptrDate up(new Date()); // 正确显式构造重载operator bool判断是否持有有效资源if(up3) { cout持有资源endl; } if(!up1) { coutup1为空endl; }6 shared_ptr共享所有权引用计数机制场景一份资源需要多个对象同时使用资源最后一个使用者销毁时才释放堆内存。6.1 引用计数为什么必须存堆上不能用静态成员错误思路把计数写成类静态static int _count。静态成员属于整个类所有 shared_ptr 对象共用同一个计数多个独立资源会互相干扰完全错误。✅正确实现 每 new 一份资源同时在堆上 new 一块独立的引用计数不同 shared_ptr 拷贝的时候共享同一份计数块。构造资源指针_ptr new 一个计数初始值为 1拷贝构造复制_ptr与计数指针_pcount(*_pcount)析构--(*_pcount)计数减到 0调用删除器释放资源同时释放计数块本身内存赋值先 release 释放自己旧资源接管对方资源与计数计数 。6.2 make_shared 原理与优势auto sp make_sharedDate(2025,10,1);make_sharedT(args...)一次内存分配同时分配对象内存 引用计数控制块内存。普通shared_ptrDate sp(new Date(...))两次堆分配①对象 ②控制块make_shared只一次堆分配减少内存碎片效率更高。注意同样 explicit禁止裸指针隐式转换// shared_ptrDate sp new Date(); //编译报错 shared_ptrDate sp(new Date()); auto sp2 make_sharedDate(2025,10,1); //推荐写法6.3 operator boolif(sp) { // 资源有效 }接口use_count()返回当前引用计数值。7 删除器 Deleter 深度讲解【面试高频坑点】智能指针默认析构执行普通delete当资源不是new出来的堆对象new [] 数组、FILE * 文件句柄、互斥锁必须自定义删除器。删除器本质任意可调用对象仿函数、函数指针、lambda 表达式。⚠unique_ptr 与 shared_ptr 删除器语法巨大差异面试常考unique_ptrT, Deleter删除器类型写在模板参数列表对象构造时可以不传删除器实例仿函数无参可以默认构造shared_ptrT模板参数没有删除器删除器对象必须作为构造函数实参传入。完整示例三种删除器写法仿函数、函数指针、lambda#includememory #includecstdio using namespace std; //1.仿函数删除器 templateclass T struct DeleteArray { void operator()(T* ptr) { delete[] ptr; } }; //2.普通函数指针 templateclass T void DelArrayFunc(T* ptr) { delete[] ptr; } //管理文件句柄仿函数 struct FClose { void operator()(FILE* fp) { cout 执行fclose endl; fclose(fp); } }; int main() { // unique_ptr 删除器模板参数指定类型 //仿函数不需要传实例默认构造 unique_ptrDate, DeleteArrayDate up1(new Date[5]); //函数指针作为删除器必须传入函数 unique_ptrDate, void(*)(Date*) up2(new Date[5], DelArrayFuncDate); //lambda删除器decltype获取lambda类型 auto lam [](Date* p){delete[] p;}; unique_ptrDate, decltype(lam) up3(new Date[5], lam); // shared_ptr 删除器构造函数传可调用对象 shared_ptrDate sp1(new Date[5], DeleteArrayDate()); shared_ptrDate sp2(new Date[5], DelArrayFuncDate); shared_ptrDate sp3(new Date[5], [](Date* p){delete[] p;}); //管理FILE*文件句柄示例 shared_ptrFILE fp(fopen(test.txt,w), FClose()); return 0; }标准库提供特化unique_ptrT[]、shared_ptrT[]内置删除器自动调用delete[]不需要手动写删除器。8 weak_ptr 弱指针解决 shared_ptr 循环引用8.1 循环引用完整复现与泄漏成因场景双向链表两个节点互相保存 shared_ptr。#includememory #includeiostream using namespace std; 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() endl; //1 cout n2.use_count() endl; //1 n1-_next n2; n2-_prev n1; cout n1.use_count() endl; //2 cout n2.use_count() endl; //2 //main结束n1、n2局部对象析构计数各减为1 //节点内部_next、_prev仍然互相持有shared_ptr计数永远不为0内存泄漏 return 0; }泄漏逻辑拆解main 函数结束局部对象 n1、n2 析构两个节点引用计数从 2 → 1节点 A 的_next是 shared_ptr指向节点 B节点 B 的_prev是 shared_ptr 指向节点 A两个节点都还有引用计数 1不会调用析构节点不析构则_next、_prev成员不会销毁形成死循环内存永远释放不了。✅修复双向关联其中一方改为weak_ptrstruct ListNode { int _data; shared_ptrListNode _next; weak_ptrListNode _prev; //weak_ptr不会增加引用计数 ~ListNode() { cout ~ListNode() 节点析构 endl; } };8.2 weak_ptr 核心特性weak_ptr 不参与资源所有权不执行 RAII不能直接 new 资源初始化 weak_ptr只能从 shared_ptr 拷贝 / 赋值构造绑定 shared_ptr 的时候不会增加 shared_ptr 的引用计数接口expired()返回 bool判断绑定的资源是否已经被释放过期lock()返回shared_ptrT资源有效返回有效的 shared_ptr资源销毁返回空 shared_ptr。❗weak_ptr没有重载operator*和operator-不能直接访问资源必须 lock 提升为 shared_ptr 之后访问防止访问已经销毁的悬空资源。int main() { shared_ptrstring sp1 make_sharedstring(hello); weak_ptrstring wp sp1; cout wp.expired() endl; //false资源存活 cout wp.use_count() endl; //use_count返回绑定的shared_ptr计数 sp1.reset(); //释放资源 cout wp.expired() endl; //true资源过期 // auto ref *wp; //编译报错weak_ptr没有解引用 auto spTemp wp.lock(); if(spTemp) { cout *spTemp endl; } return 0; }9 shared_ptr 线程安全细节【面试高频】两条必须区分的重点引用计数的修改是线程安全标准库内部引用计数使用std::atomic原子类型多线程拷贝、析构 shared_ptr增减引用计数不会发生数据竞争。被 shared_ptr 托管的业务对象本身完全没有线程安全保护多个线程读写对象里面的成员变量需要业务代码自己加互斥锁shared_ptr 不会帮你做同步。误区很多同学误以为用了 shared_ptr被管理对象多线程访问就是安全这是错误。手写简易版本如果直接用普通int* _pcount多线程下计数会乱掉要线程安全必须改成atomicint* _pcount。10 简易模拟实现课件源码仅理解原理不等于标准库完整实现注意这份模拟做简化weak_ptr 没有实现控制块lock 能力不完备仅用于理解基础原理。#includeiostream #includefunctional using namespace std; namespace bit { templateclass T class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {} auto_ptr(auto_ptrT sp) { _ptr sp._ptr; sp._ptr nullptr; } auto_ptrT operator(auto_ptrT ap) { if(this ! ap) { delete _ptr; _ptr ap._ptr; ap._ptr nullptr; } return *this; } ~auto_ptr() { delete _ptr; } T operator*(){return *_ptr;} T* operator-(){return _ptr;} private: T* _ptr; }; templateclass T class unique_ptr { public: explicit unique_ptr(T* ptr nullptr) :_ptr(ptr) {} ~unique_ptr() { delete _ptr; } // 删除拷贝 unique_ptr(const unique_ptrT sp) delete; unique_ptrT operator(const unique_ptrT sp) delete; // 移动语义 unique_ptr(unique_ptrT sp) { _ptr sp._ptr; sp._ptr nullptr; } unique_ptrT operator(unique_ptrT sp) { if(this ! sp) { delete _ptr; _ptr sp._ptr; sp._ptr nullptr; } return *this; } T operator*(){return *_ptr;} T* operator-(){return _ptr;} private: T* _ptr; }; templateclass T class shared_ptr { public: explicit shared_ptr(T* ptr nullptr) :_ptr(ptr) ,_pcount(new int(1)) {} templateclass D shared_ptr(T* ptr, D del) :_ptr(ptr) ,_pcount(new int(1)) ,_del(del) {} shared_ptr(const shared_ptrT sp) { _ptr sp._ptr; _pcount sp._pcount; _del sp._del; (*_pcount); } void release() { if(--(*_pcount) 0) { _del(_ptr); delete _pcount; _ptr nullptr; _pcount nullptr; } } shared_ptrT operator(const shared_ptrT sp) { if(_ptr ! sp._ptr) { release(); _ptr sp._ptr; _pcount sp._pcount; (*_pcount); _del sp._del; } return *this; } ~shared_ptr() { release(); } T* get() const { return _ptr; } int use_count() const { return *_pcount; } T operator*(){return *_ptr;} T* operator-(){return _ptr;} private: T* _ptr; int* _pcount; functionvoid(T*) _del [](T* p){delete p;}; }; 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; } private: T* _ptr nullptr; }; }11 内存泄漏完整讲解什么是内存泄漏程序逻辑错误堆内存已经不再被业务使用但是没有执行释放。注意不是物理内存消失进程还存在但是程序已经失去这块内存的使用权无法再次释放它。危害短生命周期程序进程结束操作系统回收进程全部虚拟内存泄漏几乎无影响。长期运行的后台服务、守护进程、服务器程序泄漏不断累积内存占用持续上涨响应越来越慢最终 OOM 被系统杀死。内存泄漏产生的典型场景new 之后忘记写 delete异常、return 提前退出delete 没有执行shared_ptr 循环引用复杂分支逻辑部分分支漏掉释放逻辑。检测工具Linuxvalgrind工具检测内存泄漏Linux下几款C程序中的内存泄露检查工具_c内存泄露工具分析-CSDN博客WindowsVLD 内存泄漏检测库。windows下的内存泄露检测工具VLD使用_windows内存泄漏检测工具-CSDN博客解决思路分两类事前预防RAII 智能指针优先尽量避免裸 new/delete编码规范事后排查泄漏检测工具定位泄漏点。12 智能指针发展历史C98auto_ptr存在严重设计缺陷Boost 库提供scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptrTR1 技术报告把 boost 部分智能指针引入非正式标准C11 正式标准unique_ptr对标 boost 的scoped_ptrshared_ptr、weak_ptr直接吸纳 boost 实现进入标准库。13 选型最佳实践与避坑清单✅推荐做法优先unique_ptr独占语义开销最小只有真正需要共享资源的时候才用shared_ptr创建 shared_ptr 优先make_shared减少内存分配次数降低内存碎片双向链表、环形结构警惕循环引用使用weak_ptr打断循环管理数组、文件句柄优先标准特化版本或者自定义删除器。❌避坑清单禁止使用auto_ptr不要把同一个原生裸指针交给两个互不相关的 shared_ptr 管理会造成双重释放weak_ptr 不能直接解引用访问资源必须 lock 提升为 shared_ptrshared_ptr 只保证引用计数线程安全托管对象读写需要自己加锁区分删除器语法unique_ptr 删除器写模板参数shared_ptr 删除器写构造实参。14 面试高频问答Q1RAII 是什么资源获取即初始化将资源交给栈对象托管构造获取资源析构释放资源无论函数正常返回还是异常栈展开栈对象析构一定会执行保证资源释放。智能指针是 RAII 典型实现。Q2auto_ptr 为什么废弃拷贝时直接转移资源管理权源对象置空拷贝后源对象悬空隐式拷贝场景极易空指针崩溃C11 废弃。Q3unique_ptr 为什么要删除拷贝构造unique_ptr 是独占所有权语义一份资源同一时刻只能被一个对象管理拷贝会破坏独占语义编译期 delete 禁止拷贝只能 std::move 移动。Q4shared_ptr 引用计数为什么不能用 static 静态成员static 属于整个类所有对象共用一份计数不同独立资源需要各自独立计数块计数必须每一份资源单独 new 在堆上。Q5shared_ptr 循环引用泄漏原理怎么解决两个对象内部互相持有 shared_ptr外部 shared_ptr 析构之后内部成员还互相持有引用计数无法降到 0资源不会释放。把其中一个成员改成 weak_ptrweak_ptr 不增加引用计数打破循环。Q6weak_ptr 为什么不能直接访问资源lock () 作用是什么weak_ptr 不拥有资源无法判断资源是否已经销毁。lock () 尝试提升返回 shared_ptr如果资源已经释放返回空 shared_ptr。expired () 用来判断资源是否过期。Q7shared_ptr 哪些部分线程安全引用计数增减是原子操作多线程拷贝析构 shared_ptr 安全被托管的业务对象本身没有任何同步保护读写成员需要用户加锁。Q8unique_ptr 和 shared_ptr 删除器的语法区别unique_ptr 删除器类型放在模板参数shared_ptr 删除器是构造函数的实参。Q9make_shared 优势是什么一次性分配内存对象与控制块放在同一块内存减少堆分配次数降低内存碎片。Q10explicit 对智能指针起到什么作用禁止裸指针隐式转换为智能指针对象防止不经意发生裸指针交给智能指针管理的隐蔽错误。