一、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(在核心间广播)。