std::shared_ptr的引用计数必须是原子的,因为多线程下拷贝、赋值、析构会并发读写同一计数,非原子操作会导致丢失自增、提前释放对象(uaf);标准强制要求原子实现,即使单线程也无法绕过,代价是额外指令与缓存行访问。

std::shared_ptr 的引用计数为什么必须是原子的?
因为 std::shared_ptr 的拷贝、赋值、析构都可能在多线程中并发发生,而这些操作都要读写同一个引用计数。如果计数不是原子的,两个线程同时执行 ++count 就可能丢失一次自增,导致提前释放对象——这是典型的 UAF(Use-After-Free)漏洞。
标准要求这个计数必须用原子操作实现,不管底层用的是 std::atomic<int></int> 还是平台级的 lock-free 指令(比如 x86 的 lock inc),目的只有一个:保证每次增减都不可分割。
- 即使你只在单线程里用
std::shared_ptr,它的引用计数类型仍是原子的,无法绕过(除非手写定制删减版) - 原子操作本身有代价:在 ARM 等弱内存序平台上,
fetch_add可能隐含 full memory barrier;x86 虽然较轻,但仍有指令开销 - 别指望编译器帮你“优化掉”原子性——它不会,也不能
引用计数开销具体有多大?
不是“慢”,而是“比裸指针多几条指令 + 一次缓存行访问”。典型场景下,一次 shared_ptr 拷贝包含:
- 对控制块中引用计数执行一次原子
fetch_add(1) - 可能触发 cache line bouncing(多个线程频繁修改同一 cache line 中的计数)
- 析构时还要再做一次原子
fetch_sub(1),并检查是否为 0 决定是否 delete 对象和控制块
实测:在主流 x86-64 上,一次 shared_ptr 拷贝比 unique_ptr 拷贝慢 2–5 倍(取决于缓存状态),但绝对耗时仍在纳秒级(~1–3 ns)。真正伤性能的是高频共享+频繁拷贝,比如在 tight loop 里反复传参或存入容器。
立即学习“C++免费学习笔记(深入)”;
什么时候可以避免引用计数开销?
如果你确定某个对象生命周期完全由单一线程管理,且不需要跨函数/模块共享所有权,就别用 std::shared_ptr。
- 优先用
std::unique_ptr:移动语义零开销,无原子操作 - 若只是临时观察,用裸指针或
std::observer_ptr(C++17,纯语法糖,不干预生命周期) - 若需跨线程传递所有权,考虑
std::move一个unique_ptr,而不是拷贝shared_ptr - 注意:把
shared_ptr存进std::vector并反复push_back,等于反复触发原子增减——这里最容易被忽略
自定义删除器或别名构造会加重开销吗?
不会额外增加引用计数本身的开销,但会影响控制块布局和缓存效率。
- 带自定义删除器的
shared_ptr必须把删除器对象存进控制块,增大控制块尺寸,可能让原本紧凑的 cache line 更容易 miss - 别名构造(如
shared_ptr<t>(p, alias)</t>)会复用原控制块,不新增计数操作,但要注意:它延长了原对象的生命周期,而你可能没意识到 - 所有这些都不改变引用计数的原子性逻辑——该原子还是原子,该慢还是慢
真正难察觉的是:控制块分配本身是堆上的,而堆分配+原子操作+缓存竞争三者叠加,在高并发场景下会让延迟毛刺明显起来。这不是 bug,是设计权衡的结果。










