彻底搞懂 shared_ptr:底层结构、原子线程安全、强弱计数、手写简易实现 本篇文章一次性彻底梳理清楚从底层内存结构、双指针模型、控制块、原子计数、线程安全、weak_ptr 原理、内存释放时机最后手写一份极简可运行的 shared_ptr / weak_ptr吃透 C 智能指针核心。一、先纠正最大误区shared_ptr 内部不是一个指针是两个1. 完整成员结构很多初学者误区shared_ptr 只是一个包装过的裸指针。完全错误。标准库中一个 shared_ptr 对象内部固定包含两个指针我们可以通过简易类声明直观看到底层成员结构// shared_ptr 极简底层声明标准库核心结构templatetypenameTclassshared_ptr{private:T*_ptr;// 1. 数据指针指向业务堆对象ControlBlock*_ctrl;// 2. 控制块指针指向独立的计数控制块};两个指针各司其职数据指针 T* _ptr指向用户真正需要管理的堆内存对象控制块指针 ControlBlock* _ctrl指向堆上独立的控制块存放引用计数、析构信息、内存释放逻辑核心关键点多个 shared_ptr 指向同一个对象时所有指针共享同一个控制块计数统一管理。这也是为什么对象销毁 ≠ 控制块销毁。二、核心控制块 ControlBlock 结构一切原理的根源1. 控制块内部核心成员控制块是独立堆内存和业务对象内存分离核心结构极简声明如下// 控制块核心极简声明structControlBlock{std::atomiclonguse_cnt;// 强引用计数std::atomiclongweak_cnt;// 弱引用计数// 类型擦除对象析构、控制块释放函数void(*destroy)(void*);void(*dealloc)(ControlBlock*);};use_count强引用计数记录当前存活的 shared_ptr 数量管控业务对象生命周期weak_count弱引用计数记录当前存活的 weak_ptr 数量管控控制块内存生命周期为什么必须是atomic 原子变量多线程下会并发读写计数普通变量会触发数据竞争导致计数错乱、双重析构、内存泄漏、程序崩溃。原子变量保证计数操作线程安全。2. 两个计数的生命周期规则很重要这是面试最高频考点也是区分初级、中级开发者的关键use_count 变为 0立即销毁托管的业务对象但不释放控制块还有 weak_ptr 需要读取控制块状态use_count 0 且 weak_count 0才会真正释放控制块内存彻底回收所有内存一句话总结对象销毁看强计数控制块销毁看强弱双计数。三、引用计数变更场景 必考1. 强计数 use_count原子递增shared_ptr 拷贝构造shared_ptr 拷贝赋值weak_ptr.lock() 成功升级为 shared_ptr2. 强计数 use_count--原子递减shared_ptr 析构shared_ptr 主动 reset()shared_ptr 被赋值为空 / 被覆盖赋值3.完全不修改任何计数移动构造、移动赋值移动语义只是转移两个指针的所有权源指针置空无任何原子操作、无计数变更性能远高于拷贝。4. 弱计数 weak_count 变更weak_ptr 创建、拷贝weak_countweak_ptr 析构、赋值覆盖weak_count--weak_ptr 永远不影响 use_count不阻止对象销毁四、shared_ptr 线程安全很多人只会背“智能指针线程安全”但不知道哪里安全、哪里不安全。1. 安全的场景多线程并发拷贝同一个 shared_ptr原子计数递增安全多线程并发析构各自的 shared_ptr原子计数递减安全多线程并发 weak_ptr.lock() / expired()CAS 原子操作安全2. 绝对不安全的场景极易踩坑多线程同时修改同一个 shared_ptr 实例p.reset()、pxxx、pnullptr原因内部的 T*、ControlBlock* 两个裸指针不是原子的并发写会指针撕裂、野指针、崩溃多线程读写托管对象本身的数据shared_ptr 只管生命周期不管业务数据竞争需要手动加锁3. 补充内存序简单理解std::memory_order_relaxed只保证变量原子性不做内存屏障允许指令自由重排。std::memory_order_release屏障当前原子操作之前的所有读写不能重排到原子操作之后。std::memory_order_acquire屏障当前原子操作之后的所有读写不能重排到原子操作之前。std::memory_order_acq_rel同时拥有 acquire release 屏障原子操作前后指令都不允许跨重排。shared_ptr内存序使用规则 底层原因1. 计数递增统一使用 relaxed场景拷贝构造、拷贝赋值、weak_ptr::lock 成功递增 use_count、weak_ptr 构造拷贝递增 weak_count。原因计数递增是纯统计行为不涉及对象销毁、内存释放没有依赖关系。指令重排不会引发BUG使用 relaxed 可以省去内存屏障开销性能最优。2. 计数递减统一使用 acq_rel场景shared_ptr 析构/reset/赋值覆盖、weak_ptr 析构/赋值覆盖。原因递减计数是资源销毁的临界操作。需要 release 保证对象析构、内存读写等前置操作一定在计数递减前完成需要 acquire 保证读取到最新计数后再执行后续的内存释放判断。杜绝指令重排导致的「先减计数、后析构对象」的致命乱序问题。3. 单纯读取计数使用 acquire场景判断 weak_count 是否为0、lock 预读 use_count、expired() 判断状态。原因必须保证读到最新的、已经落地完成的计数状态再执行后续逻辑避免读到旧数据导致误判、空指针、双重释放。五、weak_ptr 核心原理 循环引用解决方案1. weak_ptr 内部结构weak_ptr 底层内存布局和 shared_ptr完全一致同样是双指针结构极简声明如下// weak_ptr 极简底层声明templatetypenameTclassweak_ptr{private:T*_ptr;// 数据指针指向托管对象ControlBlock*_ctrl;// 控制块指针指向共享的计数控制块};唯一区别weak_ptr 不会递增强引用计数 use_cnt仅观测对象状态不持有对象生命周期无法直接访问对象。2. lock() 核心逻辑lock() 的核心就是CAS 无锁原子争抢对应核心源码就是compare_exchange_weak用来线程安全的尝试提升强引用计数// CAS 核心代码lock 关键逻辑longcntctrl_-use_cnt.load(std::memory_order_acquire);// 循环 CAS 重试防止多线程竞争失败while(cnt0){// 预期值cnt 当前内存值就更新为 cnt1// 竞争失败则刷新cnt继续重试if(ctrl_-use_cnt.compare_exchange_weak(cnt,cnt1,std::memory_order_acq_rel,std::memory_order_acquire)){// CAS 成功强计数1返回可用 shared_ptrreturnSharedPtrT(ptr_,ctrl_);}}// use_cnt 0对象已销毁returnSharedPtrT();CAS 核心逻辑通过compare_exchange_weak无锁原子操作并发安全地尝试将强计数 1避免加锁开销。while 循环作用处理多线程竞争失败和compare_exchange_weak伪失败场景CAS 失败时会通过acquire 原子内存序读取内存中 use_cnt 的最新值并覆盖更新 cnt不断自旋重试确保并发场景下逻辑绝对正确。3. 解决循环引用两个对象互相持有 shared_ptr会导致双方 use_count 永远无法归 0造成内存泄漏。解决方案将其中一方改为 weak_ptr不增加强计数对象可正常析构。六、手写极简版 shared_ptr weak_ptr可直接运行基于上面所有底层原理手写一套轻量简易版本贴合标准库核心设计逻辑代码精简易懂方便直观理解智能指针的完整工作流程。#includeatomic#includeutility// 前置声明templatetypenameTclassWeakPtr;// 控制块结构体structControlBlock{std::atomiclonguse_cnt{0};// 强计数std::atomiclongweak_cnt{0};// 弱计数// 类型擦除回调对象析构、控制块释放void(*destroy)(void*)nullptr;void(*dealloc)(ControlBlock*)nullptr;};// 简易 SharedPtr 实现templatetypenameTclassSharedPtr{private:T*ptr_nullptr;ControlBlock*ctrl_nullptr;// 私有构造仅供 WeakPtr::lock 升级使用SharedPtr(T*p,ControlBlock*c):ptr_(p),ctrl_(c){if(ctrl_){ctrl_-use_cnt.fetch_add(1,std::memory_order_relaxed);}}// 授权 WeakPtr 访问私有成员friendclassWeakPtrT;// 释放当前资源voidrelease(){if(!ctrl_){return;}// 强计数递减获取递减后的最新值ctrl_-use_cnt.fetch_sub(1,std::memory_order_acq_rel);// 为了和原理贴合直接在读取一边递减的值直观longnew_usectrl_-use_cnt.load(std::memory_order_acquire);if(new_use0){// 强计数归0销毁托管对象if(ctrl_-destroy){ctrl_-destroy(ptr_);}ptr_nullptr;// 弱计数为0彻底释放控制块longold_weakctrl_-weak_cnt.load(std::memory_order_acquire);if(old_weak0){if(ctrl_-dealloc){ctrl_-dealloc(ctrl_);}ctrl_nullptr;}}}public:// 普通构造explicitSharedPtr(T*pnullptr):ptr_(p){if(ptr_){ctrl_newControlBlock;ctrl_-use_cnt1;ctrl_-weak_cnt0;ctrl_-destroy[](void*obj){deletestatic_castT*(obj);};ctrl_-dealloc[](ControlBlock*cb){deletecb;};}}// 拷贝构造SharedPtr(constSharedPtrother):ptr_(other.ptr_),ctrl_(other.ctrl_){if(ctrl_){ctrl_-use_cnt.fetch_add(1,std::memory_order_relaxed);}}// 拷贝赋值SharedPtroperator(constSharedPtrother){if(this!other){release();ptr_other.ptr_;ctrl_other.ctrl_;if(ctrl_){ctrl_-use_cnt.fetch_add(1,std::memory_order_relaxed);}}return*this;}// 移动构造SharedPtr(SharedPtrother)noexcept:ptr_(other.ptr_),ctrl_(other.ctrl_){other.ptr_nullptr;other.ctrl_nullptr;// 移动语义不修改引用计数}// 移动赋值SharedPtroperator(SharedPtrother)noexcept{if(this!other){release();ptr_other.ptr_;ctrl_other.ctrl_;other.ptr_nullptr;other.ctrl_nullptr;// 移动语义不修改引用计数}return*this;}// 析构函数~SharedPtr(){release();}// 对外工具接口T*get()const{returnptr_;}Toperator*()const{return*ptr_;}T*operator-()const{returnptr_;}longuse_count()const{returnctrl_?ctrl_-use_cnt.load():0;}boolunique()const{returnuse_count()1;}voidreset(T*pnullptr){release();ptr_p;ctrl_nullptr;if(p){ctrl_newControlBlock;ctrl_-use_cnt1;ctrl_-weak_cnt0;ctrl_-destroy[](void*obj){deletestatic_castT*(obj);};ctrl_-dealloc[](ControlBlock*cb){deletecb;};}}};// 简易 WeakPtr 实现templatetypenameTclassWeakPtr{private:T*ptr_nullptr;ControlBlock*ctrl_nullptr;public:WeakPtr()default;// 从 SharedPtr 构造弱指针WeakPtr(constSharedPtrTsp):ptr_(sp.ptr_),ctrl_(sp.ctrl_){if(ctrl_){ctrl_-weak_cnt.fetch_add(1,std::memory_order_relaxed);}}// 拷贝构造WeakPtr(constWeakPtrother):ptr_(other.ptr_),ctrl_(other.ctrl_){if(ctrl_){ctrl_-weak_cnt.fetch_add(1,std::memory_order_relaxed);}}// 拷贝赋值WeakPtroperator(constWeakPtrother){if(this!other){if(ctrl_){ctrl_-weak_cnt.fetch_sub(1,std::memory_order_acq_rel);}ptr_other.ptr_;ctrl_other.ctrl_;if(ctrl_){ctrl_-weak_cnt.fetch_add(1,std::memory_order_relaxed);}}return*this;}// 析构~WeakPtr(){if(ctrl_){// fetch_sub 返回的是老值。所以判断需要1longoldctrl_-weak_cnt.fetch_sub(1,std::memory_order_acq_rel);// 强弱计数都为0释放控制块if(old1ctrl_-use_cnt.load()0){if(ctrl_-dealloc){ctrl_-dealloc(ctrl_);}}}}// 弱指针升级为强指针CAS 无锁线程安全SharedPtrTlock()const{if(!ctrl_){returnSharedPtrT();}longcntctrl_-use_cnt.load(std::memory_order_acquire);while(cnt0){if(ctrl_-use_cnt.compare_exchange_weak(cnt,cnt1,std::memory_order_acq_rel,std::memory_order_acquire)){returnSharedPtrT(ptr_,ctrl_);}}returnSharedPtrT();}// 判断对象是否已销毁boolexpired()const{returnctrl_?(ctrl_-use_cnt.load()0):true;}};以上手写实现为精简教学版本完整对齐 C11 标准库核心设计思想剔除了兼容冗余逻辑更适合原理学习。如需对照官方完整源码深入研读可查看微软开源 STL 标准库实现std::shared_ptr / std::weak_ptr 标准库源码七、全文核心知识点总结面试速背shared_ptr 内部两个指针数据指针、控制块指针不是一个控制块两个原子变量use_count对象生命周期、weak_count控制块生命周期计数变更拷贝加、析构/reset减、移动不动、lock加释放时机强计数0销毁对象强弱双0释放控制块线程安全边界计数操作安全同实例并发写不安全不保护业务数据weak_ptr不影响强计数、解决循环引用、lock原子升级结尾多数人学习智能指针仅停留在API调用和表层结论无法吃透底层逻辑、线程安全原理与内存释放细节面试中极易卡壳。本文从底层结构出发搭配可运行的手写精简实现完整打通 shared_ptr、weak_ptr 的强弱计数机制、原子线程安全、生命周期管理全链路逻辑。既能满足日常开发认知也可从容应对面试深挖提问彻底夯实C智能指针核心功底。