编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
        • 1. 案例引入
          • 1.1 图节点循环引用的内存泄漏
          • 1.2 enablesharedfrom_this 的双控制块惨案
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 控制块的五个字段
          • 2.2 为何这么切
        • 3. 控制块的精确布局与设计
          • 3.1 libstdc++ 实际源码逐行拆解
          • 3.2 strongcount 与 weakcount 的完整推演
          • 3.3 内存布局——make_shared vs 普通构造
          • 3.4 控制块的析构时机
        • 4. 引用计数的原子操作实现
          • 4.1 原子增与原子减汇编推演
          • 4.2 为什么不用互斥锁
          • 4.3 高竞争场景的性能退化
          • 4.4 weak_ptr::lock 的原子快照
          • 4.4 weak_ptr::lock 的 CAS 循环完整拆解
        • 5. make_shared 的单次分配原理
          • 5.0 为什么需要一次分配
          • 5.1 两次分配 vs 一次分配的全景对比
          • 5.2 Weaked Pointer 对单次分配的限制
          • 5.3 弱引用保持控制块存活
        • 6. weak_ptr 的原理与用途
          • 6.1 不增加 strong_count 的设计
          • 6.2 expired() 与 lock() 的原子语义
          • 6.3 Observer 模式与缓存场景
          • 6.4 use_count 的调试价值与陷阱
        • 7. enablesharedfrom_this 内幕
          • 7.1 weak_ptr 藏在基类里
          • 7.2 sharedfromthis 的初始化时机
          • 7.3 双控制块的预防
        • 8. 与 unique_ptr 共用的设计模式
          • 8.1 从 unique 到 shared 的语义转换
          • 8.2 Pimpl + shared_ptr
          • 8.3 异步回调生命周期
          • 8.4 shared_ptr 的常见误用模式
        • 8.5 shared_ptr 的 aliasing constructor
        • 9. shared_ptr 的性能全景
          • 9.1 构造、拷贝、析构的开销对比
          • 9.2 与 GC 语言的引用对比
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次拷贝的完整生平
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-05
目录

shared_ptr底层剖析

# 29.shared_ptr底层剖析

# 目录介绍

  • 1. 案例引入
    • 1.1 图节点循环引用
    • 1.2 enable_shared_from_this 双控制块惨案
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 控制块五字段
    • 2.2 为何这么切
  • 3. 控制块精确布局与设计
    • 3.1 libstdc++ 实际源码逐行拆解
    • 3.2 strong_count 与 weak_count 完整推演
    • 3.3 make_shared vs 普通构造布局
    • 3.4 控制块析构时机
  • 4. 引用计数的原子操作实现
    • 4.1 汇编完整推演 + LOCK 前缀原理
    • 4.2 为什么不用互斥锁
    • 4.3 高竞争场景的性能退化
    • 4.4 weak_ptr::lock 的 CAS 循环完整拆解
  • 5. make_shared 单次分配原理
    • 5.0 完整论证
    • 5.1 两次 vs 一次对比
    • 5.2 weak_ptr 对分配的限制
    • 5.3 弱引用保持控制块存活
  • 6. weak_ptr 的原理与用途
  • 7. enable_shared_from_this 内幕
  • 8. 设计模式与误用
    • 8.1 unique→shared 转换
    • 8.2 Pimpl + shared_ptr
    • 8.3 异步回调
    • 8.4 常见误用模式
    • 8.5 aliasing constructor
  • 9. shared_ptr 性能全景
    • 9.1 构造/拷贝/析构开销
    • 9.2 与 GC 对比
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓 + 共享三法则
    • 10.2 一次拷贝的完整生平
    • 10.3 设计哲学五条
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 图节点循环引用的内存泄漏

某社交网络分析引擎用有向图存用户关系——每个节点持有相邻节点的 shared_ptr。这是最典型的 C++ 内存泄漏场景——编译器不报错、静态分析工具常漏掉、valgrind 测试不运行就不会发现:

// ====== 事故代码 V1:循环引用 ======
struct Node {
    std::string name;
    std::vector<std::shared_ptr<Node>> neighbors;
    ~Node() { std::cout << "~Node " << name << '\n'; }
};

auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
a->neighbors.push_back(b);
b->neighbors.push_back(a);     // ← 循环引用!
// a.use_count() = 2, b.use_count() = 2

a.reset();                      // a 的 use_count → 1(b 还持有一个引用)
b.reset();                      // b 的 use_count → 1(a 的 neighbors[0] 还持有一个引用)
// 两个 Node 都没有被析构——内存泄露!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

困惑的核心:为什么 shared_ptr 不能自动检测循环引用?Java 的 GC 就能啊?

论证:shared_ptr 是纯引用计数——没有全局对象图扫描。它看到的只有 use_count 这个数字。当 a 引用 b、b 引用 a,两个 count 都是 2(外部 1 + 对方 1)。外部引用释放后,两个 count 各降到 1——但无法再降。编译器无法知道「这 1 个引用是谁给的」——它只看到数字。要做循环检测,需要遍历整个对象图(像 GC 那样),但这就需要:

  1. 一个全局的「所有 shared_ptr 的列表」——运行时维护开销
  2. 一个「标记-清除」的 GC 线程——不符合 C++ 的「零开销抽象」

C++ 的选择:不付这笔开销。用 weak_ptr 手动打断循环——程序员知道哪里是环,编译器不需要知道。

剖析引用链的内存细节:

初始状态(外部两个 shared_ptr):
  stack: sp_a -> [Node A: name="A", neighbors=[→B]]    use_count=1
  stack: sp_b -> [Node B: name="B", neighbors=[→A]]    use_count=1

push_back 后:
  [Node A: use_count=2] ← sp_a (1) + B.neighbors[0] (1)
  [Node B: use_count=2] ← sp_b (1) + A.neighbors[0] (1)

sp_a.reset() 后:
  [Node A: use_count=1] ← 只剩 B.neighbors[0] 持有
  [Node B: use_count=2] ← sp_b (1) + A.neighbors[0] (1)

sp_b.reset() 后:
  [Node A: use_count=1] ← B.neighbors[0] 持有(B 还活着!因为 A 引用 B)
  [Node B: use_count=1] ← A.neighbors[0] 持有(A 还活着!因为 B 引用 A)
  → 死锁在两个 use_count=1 之间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

汇编层面看不见任何线索——use_count 就是一个堆上的 atomic<int>。程序正常运行,内存分析器(如 valgrind)只能在进程退出时报告「还有内存没释放」,但说不清具体在哪个对象上形成了环。

# 1.2 enable_shared_from_this 的双控制块惨案

同一个代码库的 Session 类,作者想从成员函数里获取自身的 shared_ptr:

// ====== 事故代码 V2:双控制块 ======
struct Session : std::enable_shared_from_this<Session> {
    void on_event() {
        auto self = shared_from_this();       // 获取自己的 shared_ptr
        async_post(self);                     // 投递到异步队列
    }
};

// ❌ 错误用法:裸 new 直接构造 shared_ptr
Session* raw = new Session();
std::shared_ptr<Session> sp1(raw);           // ① 创建控制块 A
auto sp2 = raw->shared_from_this();          // ② 创建控制块 B!
// 两个控制块管理同一个对象 → use_count 各自为 1 → 第一个到 0 的析构对象
// → 第二个析构时 delete 已释放的内存 → double free
1
2
3
4
5
6
7
8
9
10
11
12
13
14

shared_from_this 返回一个新的 shared_ptr——但它读取的是 enable_shared_from_this 基类里的 weak_ptr。如果这个 weak_ptr 还没被初始化(因为对象不是通过 make_shared / shared_ptr 构造函数创建的),它为空——但即使不为空,如果它是从另一个控制块来的——两个控制块一起管理同一个对象 → 灾难。

# 1.3 七个待解疑问

① shared_ptr 的控制块到底存在哪? 包含哪些字段? libstdc++ 源码长什么样?      → 第 3 章
② 引用计数的 ++ 和 -- 是原子的吗? LOCK 前缀在缓存一致性协议层怎么工作?       → 第 4 章
③ make_shared 的一次分配具体怎么实现? 源码级流程是什么?                      → 第 5 章
④ weak_ptr 怎么做到「不增加 strong_count 但能安全提升为 shared_ptr」?        → 第 4.4/第 6 章
⑤ enable_shared_from_this 怎么工作? 双控制块怎么产生、怎么预防?             → 第 7 章
⑥ shared_ptr 的性能开销到底多大? 和 GC 比怎么样? 有什么误用模式?             → 第 8.4/第 9 章
⑦ 循环引用怎么检测? 工程中怎么避免? weak_ptr 应该用在哪?                     → 第 6 / 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 控制块的五个字段

┌────────────────────────────────────────────────────┐
│                shared_ptr 控制块                    │
├──────────────┬─────────────────────────────────────┤
│ strong_count │ 强引用计数——对象存活引用数          │
│ weak_count   │ 弱引用计数——weak_ptr 数量+1(strong) │
│ deleter      │ 删除器——如何销毁对象                │
│ allocator    │ 分配器——如何释放控制块自身           │
│ object_ptr   │ 指向被管理对象的指针                │
└──────────────┴─────────────────────────────────────┘
1
2
3
4
5
6
7
8
9

注意:控制块是堆上分配的——一个 shared_ptr 实例只有两个指针(16 字节):一个指向对象、一个指向控制块。

    shared_ptr 实例              控制块 (堆)
┌──────────────────┐       ┌──────────────────┐
│ ptr → Widget     │       │ strong_count: 3  │
│ ctrl → [控制块]   │─────►│ weak_count:   1  │
└──────────────────┘       │ deleter         │
    sizeof = 16             │ Widget*         │
                            └──────────────────┘
1
2
3
4
5
6
7

# 2.2 为何这么切

疑惑:为什么 shared_ptr 需要一个独立分配的控制块——不能把引用计数嵌在对象里吗?

论证:

  1. 强类型安全——shared_ptr<void> 可以管理任何类型的对象。如果引用计数嵌入对象,shared_ptr<void> 就不知道该从哪个偏移读计数。
  2. weak_ptr 的独立生命周期——当 strong_count 归零、对象被析构后,weak_ptr 仍然可以存在。weak_ptr 需要检查 strong_count 是否为 0——这些信息不能跟着对象一起析构,必须在独立存活的「控制块」里。
  3. make_shared 的一次分配优势——控制块可以和对象合并在一次内存分配里(第 5 章)——这正是独立控制块带来的灵活性。

结论:控制块的独立堆分配是共享所有权模型的必要代价——它解耦了对象生命周期和控制信息生命周期。


# 3. 控制块的精确布局与设计

# 3.1 libstdc++ 实际源码逐行拆解

下面是 libstdc++ 真正使用的控制块源码——注释版:

// __shared_count 的基类——引用计数的物理存储
class _Sp_counted_base {
    _Atomic_word _M_use_count;   // ① strong_count——原子 int
    _Atomic_word _M_weak_count;  // ② weak_count——原子 int
                                 //    注意:非原子变量不够——多线程同时增减

    // ③ 为什么用 int 而不是 long?标准规定 use_count() 返回 long,
    //    但实现用 int 省 4 字节。理论上 2^31 个引用足够——现实中没有场景突破。

public:
    // 构造函数——初始化双计数器
    _Sp_counted_base() noexcept
        : _M_use_count(1),    // ④ 初始值 = 1——表示「有一个 shared_ptr 引用我」
          _M_weak_count(1) {} // ⑤ 初始值 = 1——为这第一个 shared_ptr 保留的控制块存活权

    // 增加强引用(shared_ptr 拷贝构造)
    void _M_add_ref_copy() {
        __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1);  // atomic++
    }

    // 释放强引用——最关键的逻辑
    void _M_release() noexcept {
        // 原子减——如果归零,调用 dispose(析构对象)+ 弱引用释放
        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) {
            _M_dispose();              // ⑥ 先析构对象
            _M_weak_release();         // ⑦ 再释放弱引用
        }
    }

    // 弱引用释放
    void _M_weak_release() noexcept {
        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) {
            _M_destroy();              // ⑧ weak_count 归零 → 删除控制块本身
        }
    }

    virtual void _M_dispose() = 0;     // ⑨ 纯虚——由模板子类实现 delete 对象
    virtual void _M_destroy() = 0;     // ⑩ 纯虚——由模板子类实现释放控制块内存
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

关键设计点逐条详解:

④⑤ 为什么初始值是 1 而不是 0?

shared_ptr&lt;T> sp 创建时:
  use_count  = 1  → sp 持有一个强引用
  weak_count = 1  → 为「sp 还活着」事实保留的控制块存活权
                   (即使没有任何 weak_ptr,控制块也需要在 sp 存活期间存活)

sp 析构 → use_count → 0 → dispose 对象 → weak_release → weak_count → 0 → 删除控制块
1
2
3
4
5
6

如果 weak_count 初始值 = 0:sp 析构时 weak_count 已是 0 → 直接删除控制块 → _M_dispose 还没调用!对象泄露。

⑥⑦ 为什么先 dispose 再 weak_release?

顺序不能反——必须先析构对象再释放控制块内存。因为如果先释放控制块,_M_dispose() 可能访问到已释放的控制块字段(如 deleter / allocator 的虚表)。

⑨⑩ 为什么用虚函数而不是模板?

这是 shared_ptr 「类型擦除」的心脏——_Sp_counted_base 是所有控制块的共同基类,无论 shared_ptr<int> 还是 shared_ptr<std::string> 的控制块都继承自它。_M_dispose / _M_destroy 的虚函数调用是运行时分发——这 8 字节(vptr)是共享所有权必须付的代价。

// 实际存储类型——模板子类:
template <typename Tp, typename Alloc>
class _Sp_counted_ptr : public _Sp_counted_base {
    Tp* _M_ptr;                      // 指向被管理对象
    Alloc _M_alloc;                  // 分配器(用于释放控制块)
    // _M_dispose() override { delete _M_ptr; }
    // _M_destroy() override { this->~_Sp_counted_ptr(); alloc.deallocate(...); }
};
1
2
3
4
5
6
7
8

所以 shared_ptr 的总 sizeof 为何是 16:

shared_ptr 实例 (16 字节)    控制块 (堆上,~48 字节)
┌──────────────────┐       ┌──────────────────────┐
│ ptr → object     │       │ vptr → vtable (8B)   │ ← 虚函数分发
│ ctrl → [block]   │───►  │ use_count      (4B)  │
└──────────────────┘       │ weak_count     (4B)  │
                           │ padding        (0-4B)│
                           │ T* _M_ptr      (8B)  │
                           │ allocator      (8B+) │
                           └──────────────────────┘
1
2
3
4
5
6
7
8
9

一个 shared_ptr 实例 = 2 个指针(16 字节)。控制块 = 独立堆分配 + 虚表 + 原子计数器——这是共享所有权无法避免的基础设施成本。

# 3.2 strong_count 与 weak_count 的完整推演

两个计数器的语义完全不同:

计数器 含义 何时++ 何时-- 归零时的动作
strong_count 对象的「强引用」数 shared_ptr 拷贝构造 shared_ptr 析构/重置 析构对象
weak_count 弱引用 + 1(为强引用预留) weak_ptr 拷贝构造;首个 shared_ptr 构造 weak_ptr 析构/重置 释放控制块

关键:weak_count 比 weak_ptr 的数量多 1——这是为「至少有一个 shared_ptr 存活时」保留的控制块存活权。当所有 shared_ptr 析构(strong_count=0)后,只要还有 weak_ptr,控制块依然存活(weak_count>0)。

# 3.3 内存布局——make_shared vs 普通构造

① 普通构造:两次分配
  shared_ptr&lt;T> sp(new T());
  ┌──────────────┐     ┌──────────┐
  │ sp: 两个指针  │     │ T 对象    │     ← 分配 1:对象
  │ ptr ─────────┼────►│          │
  │ ctrl ────────┼──┐  └──────────┘
  └──────────────┘  │
                    │  ┌──────────────┐
                    └─►│ 控制块       │     ← 分配 2:控制块
                       │ strong_count │
                       │ weak_count   │
                       └──────────────┘

② make_shared:一次分配
  auto sp = std::make_shared&lt;T>();
  ┌──────────────┐
  │ sp: 两个指针  │     ┌──────────────┐
  │ ptr ─────────┼────►│ 控制块       │     ← 一次分配:控制块 + T 连在一起
  │ ctrl ────────┼──┐  │ strong_count │
  └──────────────┘  │  │ weak_count   │
                    │  ├──────────────┤
                    └─►│ T 对象       │
                       └──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3.4 控制块的析构时机

auto sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp;

sp.reset();           // strong_count → 0
// 此时:Widget 被析构 ✅
//       控制块仍然存活(weak_count = 1——wp 持有)

wp.reset();           // weak_count → 0
// 此时:控制块被释放 ✅
1
2
3
4
5
6
7
8
9

# 4. 引用计数的原子操作实现

# 4.1 原子增与原子减汇编推演

疑惑:shared_ptr 的拷贝和析构真的需要 LOCK 前缀吗?单线程程序为什么也付出这开销?

论证:编译器无法在编译期判断「这个 shared_ptr 是否会跨线程共享」。标准规定 shared_ptr 的引用计数必须线程安全——即使你的程序是单线程,编译器也要生成原子指令。

// shared_ptr 拷贝构造
auto sp2 = sp1;
1
2

GCC 13.2 -O2 (x86-64):

; sp2 = sp1 —— 原子增 strong_count
mov   rax, [rsi+8]           ; rax = 控制块指针(shared_ptr 偏移 +8 处)
lock  add DWORD PTR [rax], 1 ; atomic strong_count++
                              ; LOCK 前缀:锁缓存行 → 当前 core 独占
                              ; → 其他 core 的 store buffer 被刷新
                              ; → 所有 core 看到一致的强一致性序
1
2
3
4
5
6
// shared_ptr 析构
sp2.reset();
1
2
; ~sp2 —— 原子减 strong_count + 检查归零
mov   rax, [rdi+8]           ; rax = 控制块指针
lock  sub DWORD PTR [rax], 1 ; atomic strong_count--
je    .Lrelease               ; 如果归零 → 跳转到释放逻辑
ret
.Lrelease:
    ; dispose() → 析构对象
    ; weak_release():
    ;   lock sub [ctrl+4], 1  ; atomic weak_count--
    ;   je .Ldestroy          ; 如果 weak_count 也为 0 → 删除控制块
1
2
3
4
5
6
7
8
9
10

为什么需要 LOCK 前缀——从缓存一致性协议角度看:

Core 0                           Core 1
auto sp2 = std::move(sp1)        sp1.reset()
  │                                │
  ├─ lock add [ctrl], 1            ├─ lock sub [ctrl], 1
  │   ① 广播 RFO (Read For Ownership) │   ① 同样广播 RFO
  │   ② 独占缓存行                  │   ② 等待 Core 0 释放缓存行
  │   ③ 修改计数                    │   ③ 获取缓存行所有权
  │   ④ 写回                        │   ④ 修改计数

如果没有 LOCK 前缀:
  Core 0 读到 count=2 → 加 1 → count=3
  Core 1 同时读到 count=2 → 减 1 → count=1
  最终 count = 1(丢失了 Core 0 的 +1!)
  → use_count 和实际引用数不一致 → 对象早释或从不释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14

单线程的 LOCK 开销:在单核上,LOCK 前缀不会触发缓存行争抢——延迟约 1 个周期。编译器也有优化——如果它能证明 shared_ptr 确实不会被多线程访问(如函数内部的局部变量),可能在 -O3 下消掉 LOCK(但标准不要求)。

# 4.2 为什么不用互斥锁

方案 单次操作延迟 (无竞争) 单次操作延迟 (4 核竞争) 内存占用
std::atomic<int> (lock add) ~5 ns ~45-120 ns 4 字节
std::mutex (futex) ~50 ns ~2000 ns 40 字节
std::shared_mutex ~80 ns ~5000 ns 56 字节

原子变量在无竞争时只需 LOCK ADD——延迟 ≈ 一个 store 指令 + 缓存一致性协议的一次 RFO 请求。互斥锁需要:

  1. lock() → 如果无竞争走 fast path(user-space 自旋)→ ~50 ns
  2. 如果有竞争 → 进入内核的 futex wait → 上下文切换(~1000-2000 ns)

差距 10×~400× —— shared_ptr 选原子变量不是偏好,是正确选择。

# 4.3 高竞争场景的性能退化

当多个线程同时修改同一个 shared_ptr 的引用计数时,LOCK ADD 触发缓存行在多核之间跳动(cache line bouncing):

线程数 单次 copy 时间 退化原因
1 15 ns 单核——无竞争
4 45 ns 4 核轮流占缓存行
16 320 ns 缓存行在不同 socket 间跳(NUMA 效应)

减轻措施:

  • 用 std::atomic<std::shared_ptr<T>>(C++20)代替多个线程直接操作同一个 shared_ptr
  • 或每个线程持有一份自己的 shared_ptr(自然减少共享计数器的竞争)

# 4.4 weak_ptr::lock 的原子快照

std::shared_ptr<Widget> sp = wp.lock();
1

汇编本质:

; wp.lock() — 原子操作序列
retry:
    mov   eax, [ctrl + strong_count_offset]   ; 读 strong_count
    test  eax, eax
    jz    return_null                          ; 如果 = 0 → 返回空
    lock  cmpxchg [ctrl + strong_count_offset], eax+1  ; CAS 原子增
    jne   retry                                ; 如果 CAS 失败 → 重试
    ; 成功——构造 shared_ptr 返回
1
2
3
4
5
6
7
8

关键:lock cmpxchg——如果 strong_count 在 读 和 增 之间被另一个线程改成 0,CAS 失败、重试。这保证了 weak_ptr::lock() 的原子性:永远不会返回一个「strong_count 已经归零的」shared_ptr。


# 4.4 weak_ptr::lock 的 CAS 循环完整拆解

疑惑:weak_ptr::lock() 为什么需要 CAS?不能直接读 strong_count 然后 ++ 吗?

论证:如果分离成读和增两步:

// ❌ 非原子版本——有竞态窗口
std::shared_ptr<T> bad_lock() {
    if (ctrl->strong_count == 0) return nullptr;  // ① 读:count = 1
    // ⚠️ 窗口:另一个线程在 ① 和 ② 之间释放了最后一个 shared_ptr
    //          → strong_count 变成 0
    ctrl->strong_count++;                          // ② 增:count = 1
    // 成功获取——但对象刚刚已经被析构了!UB!
}
1
2
3
4
5
6
7
8

CAS 的本质:原子地「比较 + 交换」——把读和增合并为一个不可分割的操作:

std::shared_ptr<Widget> sp = wp.lock();
1

汇编本质(完整版):

; wp.lock() — 完整 CAS 循环
    mov   rax, [rdi+8]                  ; rax = 控制块指针
.Lretry:
    mov   edx, [rax]                    ; edx = strong_count (读)
    test  edx, edx                      ; 检查是否为 0
    jz    .Lreturn_null                 ; 如果 = 0 → 对象已死 → 返回空
    lea   ecx, [rdx+1]                  ; ecx = edx + 1 (期望的新值)
    lock  cmpxchg [rax], ecx            ; CAS: 如果 [rax] == edx → [rax] = ecx
                                        ;      否则 → edx = [rax] (被其他线程改了)
    jne   .Lretry                       ; CAS 失败 → edx 变了 → 重新读
    ; 成功——strong_count 原子地从 N 增到 N+1
    ; 构造 shared_ptr 并返回
.Lreturn_null:
    xor   eax, eax                      ; 返回 nullptr
    ret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

lock cmpxchg 三步语义:

lock cmpxchg [mem], reg:
  ① 原子地比较 [mem] == eax
  ② 如果相等:ZF=1, [mem] = reg
  ③ 如果不等:ZF=0, eax = [mem](被别的 core 改了!)

这和 weak_ptr::lock 的需求完美匹配:
  读到 strong_count = N (> 0)
→ CAS 尝试把 strong_count 改成 N+1
→ 如果在这之间被其他线程改成 0 → CAS 失败,ZF=0 → eax 被刷新 → 走到 test eax,eax → jz retry
→ retry 时读到 0 → 返回 nullptr ✅
1
2
3
4
5
6
7
8
9
10

三种竞争状态的全路径推演:

线程 A (lock) 线程 B (reset) 结果
读 strong_count=1 — —
CAS (1→2) — ✅ lock 成功,count=2
读 strong_count=1 reset: strong_count→0 A 的 CAS 失败([mem]=0≠eax=1)→ retry → 读到 0 → 返回 null ✅
— reset: strong_count→0 A 的 CAS 失败 → 返回 null ✅

结论:CAS 保证「只要 lock 成功返回非空 shared_ptr,strong_count 至少为 1」——对象一定活着。任何竞态条件下都不会出现「返回了一个指向已析构对象的 shared_ptr」的情况。


# 5. make_shared 的单次分配原理

# 5.0 为什么需要一次分配

疑惑:make_shared 一次分配对象+控制块有什么好处?不就是少一次 operator new 调用吗?

论证:

  1. 减少 operator new 调用——每次调用 new 都涉及:① 查找 free list ② 可能的 brk/mmap 系统调用 ③ 更新分配器内部结构。少一次 = ~20ns 节省。
  2. 缓存局部性——两次分配 = 对象和控制块分散在堆上两块区域。一次分配 = 两块紧挨着——CPU 加载控制块时顺带把对象也带进了 cache line。
  3. 减少内存碎片——两次独立的小分配 = 两个独立的碎片位。一次分配 = 一块连续内存。
  4. 减少 shared_ptr 的数量跟踪——普通构造的控制块跟踪的是独立于对象的 T* object_ptr;make_shared 版两者合一。
// 普通构造——libstdc++ 内部
template <typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args) {
    // ① 计算总大小 = 控制块 + 对象
    using ctrl_type = _Sp_counted_ptr_inplace<T, allocator<T>>;
    size_t total = sizeof(ctrl_type) + sizeof(T) + alignof(T);

    // ② 一次分配
    void* mem = operator new(total);

    // ③ placement new——控制块在低地址,对象在高地址
    auto* ctrl = ::new(mem) ctrl_type(alloc, std::forward<Args>(args)...);
    // ctrl_type 的构造函数在「控制块之后」的内存上 placement new T(args...)

    // ④ 返回 shared_ptr——指向对象和控制块
    return shared_ptr<T>(ctrl);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 5.1 两次分配 vs 一次分配的全景对比

// 两次分配
std::shared_ptr<Widget> sp(new Widget(42));
// ① operator new(sizeof(Widget))       → 分配对象
// ② operator new(sizeof(control_block)) → 分配控制块

// 一次分配
auto sp = std::make_shared<Widget>(42);
// ① operator new(sizeof(control_block) + sizeof(Widget))
//    → 一次分配对象+控制块合一
1
2
3
4
5
6
7
8
9

性能对比(GCC 13.2 -O2, 100 万次构造析构):

方式 构造时间 析构时间 内存碎片
两次分配 38 ns 28 ns 两块分散
make_shared 24 ns 20 ns 一块连续

# 5.2 Weaked Pointer 对单次分配的限制

make_shared 的一个后果:只要有任何 weak_ptr 存活,整块内存(含对象和控制块)都不能释放:

auto sp = std::make_shared<LargeWidget>();  // 一次分配 1MB(对象大)
std::weak_ptr<LargeWidget> wp = sp;

sp.reset();   // strong_count=0 → LargeWidget 的析构函数被调用 ✅
              // 但 1MB 内存不能释放——因为 weak_ptr 还活着
              // → 控制块和对象的「尸体」占据同一块内存,必须等 weak_ptr 也释放

wp.reset();   // weak_count=0 → 整块 1MB 内存被释放 ✅
1
2
3
4
5
6
7
8

如果对象本身很大(如 100MB 的图像缓存),make_shared 会导致:对象析构后 100MB 内存在 weak_ptr 存活期间一直被占着。 这时候用普通构造(两次分配)更合适——对象的 100MB 可以立刻释放,只有控制块(几十字节)被 weak_ptr 保持。

# 5.3 弱引用保持控制块存活

这是 weak_count 比实际的 weak_ptr 数量多 1 的原因:

shared_ptr 存在期间:
  weak_count = (weak_ptr 数量) + 1
                            └── 这 1 个是「对象还活着」的标记
                                strong_count 归零后,这个 +1 被移除
                                weak_count = (weak_ptr 数量)

weak_count 归零 → 控制块析构(连同 make_shared 的整块内存)
1
2
3
4
5
6
7

# 6. weak_ptr 的原理与用途

# 6.1 不增加 strong_count 的设计

std::shared_ptr<Widget> sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp;      // sp.use_count() 仍然是 1——wp 不增加 strong
1
2

weak_ptr 构造——汇编:

; wp = sp — 只增 weak_count
mov   rax, [rsi+8]           ; sp 的控制块指针
lock  add DWORD PTR [rax+4], 1  ; atomic weak_count++(偏移 4 是 weak_count)
1
2
3

# 6.2 expired() 与 lock() 的原子语义

if (!wp.expired()) {
    auto sp = wp.lock();        // ⚠️ 检查 expired() 和 lock() 之间可能被改!
    // sp 可能为空!如果另一个线程在 expired() 和 lock() 之间释放了最后一个 shared_ptr
}
// ✅ 正确用法:直接用 lock()
if (auto sp = wp.lock()) {
    // sp 一定有效——lock 原子地检查 + 增加计数
}
1
2
3
4
5
6
7
8

# 6.3 Observer 模式与缓存场景

class Cache {
    std::weak_ptr<Texture> texture_cache_;
public:
    std::shared_ptr<Texture> get_texture(const std::string& path) {
        if (auto tex = texture_cache_.lock()) return tex;   // 缓存命中
        auto tex = std::make_shared<Texture>(path);
        texture_cache_ = tex;                               // weak_ptr 不阻止释放
        return tex;
    }
};
// 当没有其他 shared_ptr 引用 Texture 时——自动释放,weak_ptr 自然过期
1
2
3
4
5
6
7
8
9
10
11

# 6.4 use_count 的调试价值与陷阱

auto sp = std::make_shared<int>(42);
std::cout << sp.use_count();   // 1 —— 仅用于调试!不可用于业务逻辑!
//   另一个线程可能在你 use_count() 返回 1 后立即释放 sp
//   然后你又继续用 sp → UB

// ✅ 正确的写法:不依赖于 use_count 的值做控制流决策
1
2
3
4
5
6

# 7. enable_shared_from_this 内幕

# 7.1 weak_ptr 藏在基类里

template <typename T>
class enable_shared_from_this {
    mutable std::weak_ptr<T> weak_this_;    // ← 核心就这一个成员
protected:
    std::shared_ptr<T> shared_from_this() {
        return std::shared_ptr<T>(weak_this_);
    }
    std::shared_ptr<const T> shared_from_this() const {
        return std::shared_ptr<const T>(weak_this_);
    }
};
1
2
3
4
5
6
7
8
9
10
11

初始化时机:weak_this_ 不是在 enable_shared_from_this 的构造函数里初始化的——是在第一个 shared_ptr 构造时由 shared_ptr 的构造函数特殊设置。

# 7.2 shared_from_this 的初始化时机

std::shared_ptr<Session> sp(new Session());
// ① new Session() → Session 构造(包括 enable_shared_from_this 的构造)
//    此时 weak_this_ 为空
// ② shared_ptr<Session> 的构造函数检测到 Session 继承自 enable_shared_from_this
//    → 设置 weak_this_ = *this(即用自己来初始化基类的 weak_ptr)
// ③ 此后任何 Session 的成员函数调用 shared_from_this() → 安全返回 shared_ptr
1
2
3
4
5
6

# 7.3 双控制块的预防

// ❌ 两个 shared_ptr 各自从同一个裸指针构造 → 两个独立控制块
Session* raw = new Session();
std::shared_ptr<Session> sp1(raw);           // 创建控制块 A + 初始化 weak_this_
std::shared_ptr<Session> sp2(raw);           // 创建控制块 B!没有初始化 weak_this_!
// sp2 完全不知道 sp1 的存在——两个控制块各自计数 → double free

// ✅ 只用一个 shared_ptr 作为起点
auto sp = std::make_shared<Session>();       // 一种方式
// 或
std::shared_ptr<Session> sp(new Session());  // 只出现一次裸 new
1
2
3
4
5
6
7
8
9
10

预防原则:任何继承 enable_shared_from_this 的类,绝对不允许裸 new 后交给两个 shared_ptr。永远用 make_shared 或唯一一个 shared_ptr(new T) 构造。


# 8. 与 unique_ptr 共用的设计模式

# 8.1 从 unique 到 shared 的语义转换

// 工厂返回 unique_ptr——调用方决定是否共享
std::unique_ptr<Widget> create_widget();

// 调用方需要共享所有权时:
auto sp = std::shared_ptr<Widget>(std::move(create_widget()));
// 或者:
auto sp = std::shared_ptr<Widget>(create_widget());  // unique_ptr 可以隐式 move 进 shared_ptr
1
2
3
4
5
6
7

# 8.2 Pimpl + shared_ptr

// widget.h
class Widget {
    struct Impl;
    std::shared_ptr<Impl> pimpl_;      // shared_ptr 允许 Widget 拷贝时共享 Impl
public:
    Widget();
    void do_something();
};
1
2
3
4
5
6
7
8

与 unique_ptr 的 Pimpl 对比:unique_ptr<Impl> 要求显式声明析构函数(在 .cpp 文件中完整定义 ~Widget() 以访问 Impl 的完整性)——shared_ptr 不需要,因为它的 deleter 是类型擦除的。

# 8.3 异步回调生命周期

void async_operation(std::shared_ptr<Session> session) {
    std::thread([session = std::move(session)] {
        session->do_work();    // session 保证在回调期间存活
    }).detach();
}
// 完全不担心 session 的内存在回调执行前被释放
1
2
3
4
5
6

# 8.4 shared_ptr 的常见误用模式

误用①:把 this 指针直接放进 shared_ptr

struct Widget {
    std::shared_ptr<Widget> get_shared() {
        return std::shared_ptr<Widget>(this);     // ❌ 灾难——新建控制块!
    }
};

auto sp = std::make_shared<Widget>();
auto sp2 = sp->get_shared();
// sp.use_count() = 1, sp2.use_count() = 1 → 两个独立控制块 → double free!
// 正确:用 enable_shared_from_this (第 7 章)
1
2
3
4
5
6
7
8
9
10

误用②:在多线程中共享非 const shared_ptr 而不加锁

// shared_ptr 的引用计数是线程安全的——但 shared_ptr 对象本身不是!
std::shared_ptr<Widget> g_sp;          // 全局 shared_ptr

// 线程 A
g_sp = std::make_shared<Widget>();     // ⚠️ 和下面同时执行
// 线程 B
auto local = g_sp;                     // ⚠️ 和上面同时执行
// 问题:g_sp 的 ptr_ 和 ctrl_ 不是原子更新的——可能读到 «ptr_ 新, ctrl_ 旧»
// → 旧控制块 + 新对象指针 → 不知道计数是什么 → UB

// ✅ 修复:用 atomic<shared_ptr<T>>(C++20) 或 mutex 保护
1
2
3
4
5
6
7
8
9
10
11

误用③:shared_ptr 循环引用中的一半用 weak_ptr

struct Parent { std::shared_ptr<Child> child; };
struct Child  { std::shared_ptr<Parent> parent; };  // ❌ 循环——都用 shared

// ✅ 最小修正——把反向引用改成 weak_ptr
struct Child  { std::weak_ptr<Parent> parent; };

// 通用规则:
//   parent → child   : unique_ptr 或 shared_ptr(独占/共享)
//   child  → parent  : 裸指针 或 weak_ptr(不拥有所有权)
1
2
3
4
5
6
7
8
9

误用④:依赖 use_count 做业务逻辑

if (sp.use_count() == 1) {
    // ⚠️ 另一个线程可能在「==1检查」和「使用 sp」之间释放了引用
    sp->modify();    // 可能 UB
}
// use_count 是诊断/调试工具——不是同步原语
1
2
3
4
5

# 8.5 shared_ptr 的 aliasing constructor

一个容易被忽略但非常强大的特性——shared_ptr 的 aliasing constructor:

struct Object { int data; };
struct Holder {
    Object obj;
    std::shared_ptr<Object> get_shared() {
        // aliasing constructor——共享 Holder 的控制块,但指向 obj
        return std::shared_ptr<Object>(shared_from_this(), &obj);
    }
};
// 只要 Holder 还活着(通过 shared_ptr),obj 的引用就有效——不需要单独管理 obj 的生命周期
1
2
3
4
5
6
7
8
9

# 9. shared_ptr 的性能全景

# 9.1 构造、拷贝、析构的开销对比

操作 裸指针 unique_ptr shared_ptr
默认构造 0 ns 0 ns 0 ns
从 new 构造 0 ns 0 ns ~30 ns(分配控制块)
拷贝 0 ns 编译错误 ~15 ns(原子增)
移动 0 ns ~1 ns(交换 ptr_) ~3 ns(交换 ptr_+ctrl_)
解引用 基准 相同 相同
析构 0 ns ~5 ns(delete) ~28 ns(原子减+条件释放)

# 9.2 与 GC 语言的引用对比

维度 C++ shared_ptr Java/C# GC
何时回收 最后一个引用离开 GC 线程未来某时
回收确定性 ✅ 确定性 ❌ 非确定性
循环引用 ❌ 手工用 weak_ptr ✅ 自动检测
每字节开销 16 字节(两个指针) 0 字节(GC 有全局开销)
每次拷贝开销 ~15 ns(lock add) 0 ns(无引用计数)

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章七个疑问,逐条作答并补充新增发现:

# 疑问 答案
① 控制块结构? 第 3 章:vptr + strong_count + weak_count + object_ptr + allocator + deleter
② 原子操作的汇编? 第 4 章:lock add / lock sub / lock cmpxchg + CAS retry
③ make_shared 的一次分配? 第 5 章:源码级实现——控制块+对象连续分配;weak_ptr 存活时整块不释放
④ weak_ptr 如何安全 lock? 第 4.4:lock() 的 CAS 原子增——三种竞态全路径推演
⑤ enable_shared_from_this? 第 7 章:基类藏 weak_ptr,shared_ptr 构造时初始化;双控制块=double free
⑥ shared_ptr 性能? 第 9 章:拷贝 ~15ns、析构 ~28ns、移动 ~3ns、解引用零额外开销
⑦ 循环引用怎么办? 第 6.3:owner→owner 用 shared_ptr,owner→observed 用 weak_ptr

新增——共享所有权三法则:

法则 内容 违反的后果
① 一个对象只能有一个控制块 裸 new→多个 shared_ptr→double free
② 循环引用 = 泄露 任何环至少一端用 weak_ptr
③ shared_ptr 本身不是线程安全的 多线程改同一 shared_ptr 须用 atomic/mutex

案例①修复(循环引用):

struct Node {
    std::string name;
    std::vector<std::weak_ptr<Node>> neighbors;  // ← weak_ptr 不增加 strong_count
};

auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
a->neighbors.push_back(b);
b->neighbors.push_back(a);
// a.use_count() = 1, b.use_count() = 1  ✅ 没有循环引用
1
2
3
4
5
6
7
8
9
10

验证修复的方法:在破坏循环引用的 weak_ptr 端打印 expired()。当外部引用释放后,expired() == true 确认环已断开。

案例②修复(双控制块):永远用 make_shared 创建继承 enable_shared_from_this 的对象;如果不允许(如需要自定义 deleter),确保裸指针只进入一个 shared_ptr 构造函数。

# 10.2 一次拷贝的完整生平

auto sp2 = sp1;         // shared_ptr 拷贝

═══════ 语义层 ═══════

sp1 已经持有一个 Widget——use_count = N
sp1 的成员:
  ptr_  → Widget 对象
  ctrl_ → 控制块(堆上)

sp2 被默认构造:
  ptr_  = nullptr
  ctrl_ = nullptr

拷贝构造 sp2(sp1):
  ① 读 sp1.ptr_ 和 sp1.ctrl_
  ② ctrl_->use_count 原子 +1(lock add)
  ③ sp2.ptr_ = sp1.ptr_
  ④ sp2.ctrl_ = sp1.ctrl_

═══════ 汇编层 ═══════

    mov   rax, [rsi]         ; sp2.ptr_ = sp1.ptr_
    mov   [rdi], rax
    mov   rax, [rsi+8]       ; rax = sp1.ctrl_
    mov   [rdi+8], rax       ; sp2.ctrl_ = sp1.ctrl_
    lock  add DWORD [rax], 1 ; ctrl->use_count++ (原子)

总指令:5 条
总时间:~15 ns(4 条 mov + 1 条 lock add)
额外开销 vs unique_ptr:+2 条 mov(多一个 ctrl_ 拷贝)+ 1 条 lock add
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 10.3 设计哲学回扣

哲学 1:共享是最后的选择——默认独占

shared_ptr 有原子操作开销、控制块开销、循环引用风险、虚函数调用开销——每一项都是独占所有权不需要的。能用 unique_ptr 解决的,绝不用 shared_ptr。共享所有权是成本最高的所有权模型——只在真正需要时用。 80% 的智能指针用法应该是 unique_ptr。

哲学 2:弱引用打破循环——不拥有、只观察

weak_ptr 不是性能工具——是正确性工具。它的设计目标:在不增加强引用计数的前提下,安全地观察一个可能已经析构的对象。 CAS 是实现这条语义的唯一正确方式——任何更简单的方案(读+增两步)都有竞态窗口。

哲学 3:控制块是所有权声明的物理化

为什么 shared_ptr 需要 16 字节(两个指针)?因为共享所有权的物理体现就是:一个指针指向我、一个指针指向「我们多少人在看同一个对象」。所有权的模型决定了内存成本的底线——没有「零开销的共享所有权」这种东西。 要么付原子操作的 CPU 成本,要么付 GC 线程的全局开销。

哲学 4:一次分配 = 缓存友好 + 减少碎片 + 类型安全

make_shared 的一次分配不仅省了 operator new → 更重要的是对象和控制块在同一个 cache line 里。CPU 加载控制块时顺带加载了对象——对高频访问场景是隐形加速。

哲学 5:引用计数 = 确定性回收 + 分布式成本

shared_ptr 的引用计数回收是确定性的(对象在最后一个引用离开时立即析构)——这和 Java/C# GC 的「未知的未来某刻」完全不同。代价是每次拷贝都要原子操作。确定性 vs 吞吐量——C++ 选择了确定性。

# 10.4 速查表合集

三种智能指针速查:

维度 unique_ptr shared_ptr weak_ptr
所有权 独占 共享(计数) 不拥有
sizeof 8 字节 16 字节 16 字节
拷贝 禁止 原子增计数 原子增弱计数
移动 交换 ptr_ 交换 ptr_+ctrl_ 同 shared_ptr
析构逻辑 delete 对象 原子减→归零→delete 原子减弱计数

weak_ptr 使用法则:

循环引用的标准解法:
  owner → owner     : shared_ptr
  owner → observer  : weak_ptr
  parent → child    : unique_ptr
  child → parent    : 裸指针 或 weak_ptr
1
2
3
4
5

shared_ptr 误用速查:

误用 后果 修复
同一个裸指针给两个 shared_ptr double free make_shared
多线程改同一个 shared_ptr 不加锁 UB atomic<shared_ptr> / mutex
use_count 做业务逻辑 race condition 锁/lock()
enable_shared_from_this + 裸 new 双控制块 make_shared

下一篇:共享所有权的引用计数说清了。下一篇进入 30.weak_ptr 与 this 增强——enable_shared_from_this 的 CRTP 实现细节、weak_from_this 的引入(C++17)、二级指针失效检测、Observer 场景的最佳实践。

上次更新: 2026/06/10, 11:13:41
unique_ptr原理剖析
weak_ptr与this增强

← unique_ptr原理剖析 weak_ptr与this增强→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式