C++ 中 shared ptr 详解:原理与线程安全性分析

发布时间:2026/7/26 10:44:30

C++ 中 shared ptr 详解:原理与线程安全性分析
C 中 shared_ptr 详解原理与线程安全性分析一、引言共享所有权的智能指针std::shared_ptr是 C11 引入的智能指针它实现了共享所有权(Shared Ownership)语义——多个shared_ptr可以同时拥有同一个对象通过引用计数(Reference Counting)来追踪所有者的数量。当最后一个shared_ptr被销毁时管理的对象会被自动释放。与unique_ptr的独占所有权不同shared_ptr适用于需要多个所有者共享同一对象的场景。但共享所有权带来了额外的复杂性引用计数的存储、原子操作的开销、以及常见的循环引用问题(需要weak_ptr打破)。二、核心概念速览| 维度 | 说明 ||------|------|| 所有权模型 | 共享(多个 shared_ptr 可拥有同一对象) || 引用计数 | 追踪当前有多少个 shared_ptr 共享同一对象 || 拷贝语义 | 深拷贝(增加引用计数) || 移动语义 | 支持(转移所有权不改变引用计数) || 对象释放时机 | 最后一个 shared_ptr 销毁时(引用计数归零) || 线程安全性 | 引用计数操作是线程安全的但被管理对象本身不是 || 循环引用 | 导致内存泄漏需用 weak_ptr 打破 || 内存开销 | 通常为裸指针的 2 倍(对象指针 控制块指针) || 性能开销 | 拷贝/析构涉及原子引用计数操作 |三、shared_ptr 的内部结构3.1 控制块和对象shared_ptr内部包含两个指针对象指针指向被管理的对象控制块指针指向控制块(Control Block)包含引用计数、弱引用计数、删除器等信息控制块(0x2000)被管理对象(0x1000)shared_ptr2shared_ptr1对象指针: 0x1000控制块指针: 0x2000对象指针: 0x1000控制块指针: 0x2000T 对象shared_count: 2(强引用计数)weak_count: 1(弱引用计数)删除器分配器3.2 简化实现templatetypename T class shared_ptr { private: T* ptr_; // 指向被管理的对象 control_block* cb_; // 指向控制块 struct control_block { size_t shared_count; // 强引用计数 size_t weak_count; // 弱引用计数 // 删除器和分配器... }; public: // 构造函数 explicit shared_ptr(T* p) : ptr_(p), cb_(new control_block{1, 0}) { } // 拷贝构造函数 shared_ptr(const shared_ptr other) : ptr_(other.ptr_), cb_(other.cb_) { cb_-shared_count; // 增加引用计数 } // 析构函数 ~shared_ptr() { --cb_-shared_count; if (cb_-shared_count 0) { delete ptr_; // 删除被管理对象 if (cb_-weak_count 0) { delete cb_; // 删除控制块 } } } // 拷贝赋值运算符 shared_ptr operator(const shared_ptr other) { if (this ! other) { // 先减少当前对象的引用计数 if (--cb_-shared_count 0) { delete ptr_; if (cb_-weak_count 0) delete cb_; } // 再增加新对象的引用计数 ptr_ other.ptr_; cb_ other.cb_; cb_-shared_count; } return *this; } T* get() const { return ptr_; } T* operator-() const { return ptr_; } T operator*() const { return *ptr_; } size_t use_count() const { return cb_-shared_count; } };四、引用计数的生命周期对象控制块(引用计数)shared_ptr bshared_ptr a代码对象控制块(引用计数)shared_ptr bshared_ptr a代码a 和 b 共享同一对象a 析构但不释放对象auto a make_sharedT()shared_count 1创建对象auto b a (拷贝)shared_count 2a 离开作用域shared_count 1b 离开作用域shared_count 0delete 对象控制块也被释放(若无 weak_ptr)五、线程安全性分析5.1 引用计数的线程安全shared_ptr的引用计数操作是线程安全的。C 标准要求shared_ptr的引用计数使用原子操作因此多个线程同时拷贝/销毁指向同一对象的shared_ptr不会导致引用计数损坏。#include memory #include thread #include vector #include iostream int main() { auto sp std::make_sharedint(42); std::vectorstd::thread threads; for (int i 0; i 100; i) { threads.emplace_back([sp]() { // 按值捕获增加引用计数 // sp 的拷贝/析构是线程安全的 auto local sp; // 引用计数原子递增 // local 析构时引用计数原子递减 }); } for (auto t : threads) { t.join(); } std::cout Final use_count: sp.use_count() std::endl; // 1 // 引用计数正确没有被破坏 }5.2 被管理对象的线程安全重要区分引用计数的线程安全不等于被管理对象的线程安全。#include memory #include thread #include iostream class Counter { public: int value_ 0; void increment() { value_; } // 不是线程安全的 }; int main() { auto counter std::make_sharedCounter(); std::vectorstd::thread threads; for (int i 0; i 1000; i) { threads.emplace_back([counter]() { for (int j 0; j 1000; j) { counter-increment(); // ❌ 数据竞争 // shared_ptr 的引用计数是安全的 // 但 Counter::value_ 的修改不是线程安全的 } }); } for (auto t : threads) t.join(); std::cout Final value: counter-value_ std::endl; // 期望 1,000,000实际结果不确定(数据竞争) }正确做法对共享对象本身加锁保护或使用原子操作。#include atomic class ThreadSafeCounter { std::atomicint value_{0}; public: void increment() { value_; } // 原子操作线程安全 int get() const { return value_.load(); } }; // 或使用互斥锁 class MutexCounter { int value_ 0; mutable std::mutex mtx_; public: void increment() { std::lock_guardstd::mutex lock(mtx_); value_; } };5.3 线程安全性总结| 操作 | 线程安全性 | 说明 ||------|:---:|------|| 拷贝 shared_ptr | 安全 | 引用计数原子递增 || 析构 shared_ptr | 安全 | 引用计数原子递减 || 移动 shared_ptr | 安全 | 不涉及引用计数修改 || 修改被管理对象 |不安全| 需要额外的同步机制 || use_count() | 安全(但值可能过期) | 返回值是近似值 || reset() | 安全 | 原子地替换管理的对象 |六、make_shared 与直接构造的差异6.1 内存布局对比// 方式一直接构造(两次内存分配) std::shared_ptrint sp1(new int(42)); // 1. new int(42) — 分配 int 对象 // 2. new control_block — 分配控制块 // 总共 2 次内存分配 // 方式二make_shared(一次内存分配) auto sp2 std::make_sharedint(42); // 1. 分配一块连续内存包含 int 对象和控制块 // 总共 1 次内存分配make_shared(单块内存)连续内存块[控制块 | int 对象]shared_ptrint 对象部分控制块部分直接构造(newintnewcontrol_block)控制块shared_count: 1shared_ptrint 对象6.2 make_shared 的优势与劣势| 维度 | make_shared | 直接构造 ||------|:---:|:---:|| 内存分配次数 | 1 次 | 2 次 || 缓存局部性 | 好(对象和控制块相邻) | 差 || 异常安全 | 是 | 可能有风险 || 自定义删除器 | 不支持 | 支持 || 内存释放时机 | 可能延迟(弱引用存在时) | 对象及时释放 |make_shared 的一个陷阱如果有weak_ptr存在即使所有shared_ptr都销毁了整个内存块(包括对象)也不会释放直到所有weak_ptr也销毁。auto sp std::make_sharedint(42); std::weak_ptrint wp sp; sp.reset(); // shared_count 归零对象析构 // 但内存块没有被释放因为 weak_count 0 // 需要等到 wp 也销毁才能释放内存七、enable_shared_from_this#include memory #include iostream // 错误做法从 this 创建多个 shared_ptr class BadExample { public: std::shared_ptrBadExample getShared() { return std::shared_ptrBadExample(this); // ❌ 危险 // 每次调用都创建新的控制块 // 导致同一对象有多个独立的引用计数 } }; // 正确做法继承 enable_shared_from_this class GoodExample : public std::enable_shared_from_thisGoodExample { public: std::shared_ptrGoodExample getShared() { return shared_from_this(); // 安全共享同一个控制块 } static std::shared_ptrGoodExample create() { return std::shared_ptrGoodExample(new GoodExample()); } private: GoodExample() default; // 私有构造函数强制使用 create() }; int main() { auto obj GoodExample::create(); auto obj2 obj-getShared(); // 正确共享同一个控制块 std::cout use_count: obj.use_count() std::endl; // 2 }八、循环引用与 weak_ptr#include memory #include iostream struct B; struct A { std::shared_ptrB bPtr; ~A() { std::cout A destroyed std::endl; } }; struct B { std::shared_ptrA aPtr; // 导致循环引用 // 应该使用std::weak_ptrA aPtr; ~B() { std::cout B destroyed std::endl; } }; int main() { auto a std::make_sharedA(); auto b std::make_sharedB(); a-bPtr b; b-aPtr a; // 循环引用 // a 和 b 离开作用域 // 引用计数都不为 0 → 内存泄漏 // 不会输出 A destroyed 或 B destroyed }循环引用A::bPtrB::aPtr离开作用域引用计数-1剩余:1离开作用域引用计数-1剩余:1shared_ptrA引用计数: 2(main 中的 a B::aPtr)shared_ptrB引用计数: 2(main 中的 b A::bPtr)main 中的 amain 中的 b两者引用计数都不为 0永远无法销毁解决方案使用weak_ptr打破循环struct B { std::weak_ptrA aPtr; // weak_ptr 不增加引用计数 ~B() { std::cout B destroyed std::endl; } };九、总结std::shared_ptr是 C 中实现共享所有权的智能指针其核心原理和注意事项如下内部结构shared_ptr包含对象指针和控制块指针。控制块存储引用计数(shared_count)、弱引用计数(weak_count)、删除器和分配器。多个shared_ptr共享同一控制块。引用计数机制拷贝构造/赋值时原子递增引用计数析构时原子递减引用计数。当引用计数归零时删除被管理对象当弱引用计数也归零时删除控制块。线程安全性引用计数操作是线程安全的(使用原子操作)被管理对象的访问不是线程安全的需要额外的同步机制多个线程同时拷贝/销毁指向同一对象的shared_ptr是安全的多个线程同时修改被管理对象的状态是不安全的最佳实践优先使用std::make_sharedT()创建shared_ptr使用std::weak_ptr打破循环引用需要从this获取shared_ptr时继承std::enable_shared_from_this不要混淆引用计数的线程安全与被管理对象的线程安全如果不需要共享所有权优先使用std::unique_ptr(更轻量)shared_ptr是 C 中功能最强大的智能指针但也带来了最大的开销。理解其内部机制和线程安全性边界才能在正确的场景中安全、高效地使用它。

相关新闻

深入解析SSI同步串行接口:架构、帧格式与实战配置

深入解析SSI同步串行接口:架构、帧格式与实战配置

2026/7/26 10:44:30

1. 同步串行接口(SSI)的核心价值与设计哲学 在嵌入式开发的世界里,处理器与外设之间的通信是构建任何复杂系统的基石。无论是读取传感器数据、驱动显示屏,还是与存储芯片交互,高效、可靠的数据传输机制都至关重要。在众…

如何在3分钟内完成BetterNCM插件管理器完整安装:网易云音乐功能扩展终极指南

如何在3分钟内完成BetterNCM插件管理器完整安装:网易云音乐功能扩展终极指南

2026/7/26 10:34:30

如何在3分钟内完成BetterNCM插件管理器完整安装:网易云音乐功能扩展终极指南 【免费下载链接】BetterNCM-Installer 一键安装 Better 系软件 项目地址: https://gitcode.com/gh_mirrors/be/BetterNCM-Installer 你是否厌倦了网易云音乐PC客户端的单调功能&am…

digiCamControl高级拍摄技巧:HDR合成与焦点堆叠实战教程

digiCamControl高级拍摄技巧:HDR合成与焦点堆叠实战教程

2026/7/26 10:34:30

digiCamControl高级拍摄技巧:HDR合成与焦点堆叠实战教程 【免费下载链接】digiCamControl DSLR camera remote control open source software 项目地址: https://gitcode.com/gh_mirrors/di/digiCamControl digiCamControl是一款功能强大的单反相机远程控制开…

网盘下载限速破解:从兑换码到高效工作流实践

网盘下载限速破解:从兑换码到高效工作流实践

2026/7/26 11:44:33

昨天下午,我帮一个朋友下载一份 3GB 的设计素材包。他用的是某主流网盘,点开下载链接,跳出来的不是进度条,而是一个熟悉的弹窗——“试用加速”或“开通会员”。他叹了口气,问我有没有办法“绕过去”。我看着他屏幕上那…

LLM性能优化实战:从提示词到模型蒸馏

LLM性能优化实战:从提示词到模型蒸馏

2026/7/26 11:44:33

1. 为什么LLM性能优化如此重要?去年我在开发一个智能客服系统时,曾遇到一个典型问题:同样的GPT-3.5模型,在测试环境中响应速度飞快,但上线后平均响应时间却从1.2秒骤增到4.5秒。经过两周的排查,最终发现问题…

基于改进YOLO的老年人跌倒实时监测系统设计与优化

基于改进YOLO的老年人跌倒实时监测系统设计与优化

2026/7/26 11:44:33

1. 项目背景与核心价值 老年人跌倒监测是智慧养老领域的关键技术痛点。根据临床研究数据,65岁以上老年人每年跌倒发生率超过30%,而跌倒后未能及时救助将导致严重后果。传统监控方案存在响应延迟、隐私泄露等问题,而基于计算机视觉的智能监测系…

开源风扇控制神器:告别噪音困扰的终极解决方案

开源风扇控制神器:告别噪音困扰的终极解决方案

2026/7/26 11:44:33

开源风扇控制神器:告别噪音困扰的终极解决方案 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending/fa/FanCon…

智能写作工具paperzz提升硕士论文效率全攻略

智能写作工具paperzz提升硕士论文效率全攻略

2026/7/26 11:44:33

1. 论文写作的痛点与智能辅助的崛起写硕士论文大概是每个研究生最头疼的事情。去年帮学弟改论文时,他给我看了手机里的备忘录——光是"文献综述怎么写"这个问题就搜索了17次。这种焦虑不是个例,根据我接触过的上百名硕士生,90%的人…

深入解析CC13x2/CC26x2射频核心:中断、队列与CSMA-CA硬件加速机制

深入解析CC13x2/CC26x2射频核心:中断、队列与CSMA-CA硬件加速机制

2026/7/26 11:34:32

1. 项目概述与核心价值在嵌入式无线开发领域,尤其是基于IEEE 802.15.4标准的低功耗网络(如Zigbee、Thread),开发者常常面临一个核心挑战:如何让主控MCU高效、实时地处理射频收发事件,同时确保在复杂的无线环…

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

2026/7/26 0:04:02

目标:电脑作为RTSP 服务端,循环推送 H264/H265 视频流; RDK X5 通过 rtsp2display 拉流预览,完全不需要在开发板编译 live555。 提供两套成熟方案: ✅ 方案 A:FFmpeg(最简单,优先推…

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

2026/7/26 0:04:02

一、背景与测试方案 在实际项目交付中,PDF文件合并与版权保护水印的叠加是一个高频但容易被低估的技术需求。典型的处理链路涉及:多源PDF的文件流合并、页面级水印渲染(含透明度混合与图层叠加)、输出文件体积控制。看似简单的操作…

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

2026/7/26 0:04:02

说实话,提到PDF拆分再压缩,我真是被折腾得够呛。 上个月公司年度合同归档,一份300多页的PDF总合同,需要按年份拆分成三个独立文件,再分别压缩到10MB以内方便邮件发送各部门确认。我心想这还不简单?先找个海…

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

2026/7/26 0:04:02

目标:电脑作为RTSP 服务端,循环推送 H264/H265 视频流; RDK X5 通过 rtsp2display 拉流预览,完全不需要在开发板编译 live555。 提供两套成熟方案: ✅ 方案 A:FFmpeg(最简单,优先推…

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

2026/7/26 0:04:02

一、背景与测试方案 在实际项目交付中,PDF文件合并与版权保护水印的叠加是一个高频但容易被低估的技术需求。典型的处理链路涉及:多源PDF的文件流合并、页面级水印渲染(含透明度混合与图层叠加)、输出文件体积控制。看似简单的操作…

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

2026/7/26 0:04:02

说实话,提到PDF拆分再压缩,我真是被折腾得够呛。 上个月公司年度合同归档,一份300多页的PDF总合同,需要按年份拆分成三个独立文件,再分别压缩到10MB以内方便邮件发送各部门确认。我心想这还不简单?先找个海…