文档目录

一、16 字节的物理组成

shared_ptr<int> sp(new int(42));
栈上 (sp 对象本身,16 字节):
+──────────────────────+──────────────────────+
| ptr (8字节)          | control_block_ptr     |
| 指向堆上的 int       | 指向控制块            |
+──────────────────────+──────────────────────+

堆上:
+──────────────────────+  ← ptr 指向这里
| int: 42              |
+──────────────────────+

+──────────────────────+  ← control_block_ptr 指向这里(控制块)
| ref_count (原子 int)  |  引用计数
| weak_count (原子 int) |  弱引用计数
| deleter (函数指针)     |  删除器
| allocator (函数指针)   |  分配器
+──────────────────────+

两个堆分配:new int(42) 一次 + 控制块分配一次。这是两 new,两 delete。

二、std::make_shared 的合并优化

auto sp = std::make_shared<int>(42);
堆上 (一次 malloc):
+──────────────────────────────────────────────+
| int: 42    | ref_count | weak_count | ...    |
+──────────────────────────────────────────────+
              ↑
栈上 sp: [ptr→这里 | control_block_ptr→这里]
        ptr 指向的地址 = control_block_ptr 指向的地址(在同一个内存块内)

缓存优势:对象和控制块在同一个缓存行内。当 sp 被拷贝时,引用计数 ++ 的操作大概率在 L1 缓存中完成,不需要去主存加载控制块。

内存劣势:整个块直到 weak_count 也归零才释放。如果 shared_ptr 和 weak_ptr 混用,即使 shared_ptr 全释放了,int 的内存也不能归还给系统。


三、引用计数的原子操作

auto sp1 = std::make_shared<int>(42);
auto sp2 = sp1; // 拷贝 → 控制块 ref_count.fetch_add(1)
sp1.reset();    // → ref_count.fetch_sub(1),如果是 0 则 delete 对象
sp2.reset();    // → ref_count.fetch_sub(1),如果是 0 则 delete 控制块

每次拷贝/析构都涉及一次 fetch_add/fetch_sub(底层是 lock xadd 或 LL/SC 循环)。这就是 shared_ptr 比 unique_ptr 慢的原因——不是慢在"引用计数本身",而是每次拷贝和析构都有一次原子 RMW 操作,这个操作会触发缓存行 RFO(在核心间广播)。