彻底搞懂 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智能指针核心功底。

相关新闻

TI Sitara EMAC硬件QoS与帧分类:嵌入式网络确定性保障实践

TI Sitara EMAC硬件QoS与帧分类:嵌入式网络确定性保障实践

1. 项目概述与核心价值在嵌入式网络开发,尤其是工业控制、车载电子或实时音视频传输这类对网络延迟和确定性要求极高的领域,单纯依靠软件协议栈来处理数据包的优先级是远远不够的。软件处理会引入不可预测的调度延迟,当网络流量突发时&#x…

2026/10/20 6:05:39 阅读更多 →
AI 自动操控电脑 OpenClaw 2.7.9 部署排坑与长期使用技巧汇总(含安装包)

AI 自动操控电脑 OpenClaw 2.7.9 部署排坑与长期使用技巧汇总(含安装包)

🦞OpenClaw(小龙虾)部署实战教程|搭建属于自己的本地桌面数字员工 适配平台:Windows10/11(64 位)、macOS12 及以上 稳定版本:v2.7.9 特点✨:图形化引导部署、无需命令行…

2026/10/20 2:31:34 阅读更多 →
新手搭建桌面智能自动化 OpenClaw v2.7.9 解压部署完整流程(含安装包)

新手搭建桌面智能自动化 OpenClaw v2.7.9 解压部署完整流程(含安装包)

🦞 OpenClaw 本地 AI 自动化平台|图形化一键部署,省去繁琐环境配置 支持系统:Windows10/11 64 位、macOS12 及以上 当前稳定版本:v2.7.9 核心亮点✨:全程可视化操作,不用掌握命令行&#xff0c…

2026/10/18 5:44:32 阅读更多 →

最新新闻

智能体最新进展全景:工具调用、记忆管理与多智能体协作避坑指南

智能体最新进展全景:工具调用、记忆管理与多智能体协作避坑指南

最近几个月,智能体(Agent)方向的论文数量多到让人有点消化不良。我大概统计了一下,光是在几个主流预印本库上,标题里带“Agent”或“智能体”的文章每周都有上百篇,内容从工具调用、记忆管理到多智能体协作…

2026/10/20 6:05:24 阅读更多 →
通信感知约束下多智能体编队控制的MATLAB仿真与避坑指南

通信感知约束下多智能体编队控制的MATLAB仿真与避坑指南

上个月帮某课题组搭多智能体编队控制的仿真环境,十几台Agent要在二维平面里排成指定的圆环队形,同时还要满足通信感知约束——每台智能体只能和通信半径之内的邻居交换信息。一开始我觉得这没什么,无非是把全局通信改成局部通信,多…

2026/10/20 6:05:24 阅读更多 →
别只顾压缩Prompt!Agent少踩坑才是真正的省钱之道

别只顾压缩Prompt!Agent少踩坑才是真正的省钱之道

干Agent项目一年多,我最大的一个感受是:很多团队嘴上说的是优化成本,手上做的却是跟Prompt字数较劲。今天这篇内容,我想把这个账摊开算清楚——为什么让Agent少踩坑,比压缩Prompt更省钱。起因是我带过一个内部工单分类…

2026/10/20 6:05:24 阅读更多 →
从单表到24分表:业务层分表改造的完整实战解析

从单表到24分表:业务层分表改造的完整实战解析

前阵子刚带团队把我们内部的一套核心数据链路,从单表硬生生拆成了24张分表,业务层也跟着做了一轮彻底改造。项目代号就叫“24分表业务层改造”,从设计到上线前后折腾了两周多。这活儿看起来简单,不就是算个哈希、拼个表名嘛&#…

2026/10/20 6:05:24 阅读更多 →
上下文图谱:破解超大组织里智能体落地的断点难题

上下文图谱:破解超大组织里智能体落地的断点难题

1. 十万人的组织里,智能体到底卡在哪一环这两年“智能体”几乎成了企业数字化的标配提法:让AI理解任务、拆解流程、调用工具、给出答案。但真把它放到十万人员工的巨型组织里,事情就变得完全不一样了。我接触过不少负责企业AI基础设施的团队&…

2026/10/20 6:05:24 阅读更多 →
CRC计算源码选型指南:参数模型、查表法与逐位法实现及避坑实践

CRC计算源码选型指南:参数模型、查表法与逐位法实现及避坑实践

简介:这份循环冗余校验码(CRC)计算源代码合集面向嵌入式开发、通信协议实现与数据校验方向的学习者,帮助理解并落地CRC-8、CRC-16、CRC-32等常见算法的软件实现与硬件映射思路。压缩包共1220个文件,约6.65MB&#xff0…

2026/10/20 6:04:24 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →