仅用 std::atomic 裸整数无法安全实现引用计数,因它不解决生命周期绑定、强/弱引用分离及释放顺序问题;std::shared_ptr 通过独立控制块中两个原子计数(strong/weak)配合内存序来正确管理。

直接用 std::atomic<int></int> 做引用计数是可行的,但仅靠它无法安全实现完整的引用计数逻辑——必须配合控制块(control block)和正确的内存序,否则会崩溃或泄漏。
为什么不能只用 std::atomic<int></int> 存引用计数
裸原子整数能保证计数增减本身不丢值,但它不解决两个关键问题:
- 计数器和被管理对象的生命周期绑定关系:如果对象已析构,而另一个线程还在对已释放的
std::atomic<int>*</int>执行fetch_add,就是野指针访问 - 强/弱引用分离:像
std::shared_ptr需要区分“谁在用对象”(强引用)和“谁在观察对象”(弱引用),单个原子整数无法承载两套独立计数 - 释放顺序:对象销毁和控制块销毁必须严格按序,否则可能先删对象、后读控制块里的弱计数,触发未定义行为
std::shared_ptr 内部怎么用原子操作做引用计数
它把引用计数放在独立的控制块里,所有 std::shared_ptr 实例共享该块,而非各自持有一份计数。核心结构如下:
- 控制块中含两个
std::atomic<long></long>:一个存强引用数(strong_count),一个存弱引用数(weak_count) - 每次拷贝
std::shared_ptr:对strong_count.fetch_add(1, std::memory_order_relaxed) - 每次析构
std::shared_ptr:对strong_count.fetch_sub(1, std::memory_order_acq_rel),若结果为 0,则调用自定义删除器或delete对象 - 弱引用增减用
weak_count,且只有当strong_count == 0 && weak_count == 1时才真正释放控制块
注意:std::memory_order_acq_rel 在 fetch_sub 中是必要的——它确保对象析构前,所有对该对象的写操作都已完成并对其它线程可见。
手写简易原子引用计数时最容易踩的坑
如果你真要自己实现(比如为特定小对象定制),以下几点必须手动处理:
-
std::atomic<int></int>初始化必须用ATOMIC_VAR_INIT或构造函数显式初始化,不能靠全局零初始化——某些平台静态初始化顺序不可靠 - 递减后检查是否归零,**不能用
load()再比较**,必须用fetch_sub(1)的返回值判断,否则存在竞态窗口 - 对象销毁和计数器销毁必须分离:先原子递减强计数 → 若为 0 则销毁对象 → 再原子递减弱计数 → 若为 0 则销毁控制块
- 避免循环引用:两个对象互相持有
std::shared_ptr,会导致强计数永远不归零;改用std::weak_ptr打破闭环
std::atomic<:shared_ptr>></:shared_ptr> 不是用来做引用计数的
这是常见误解。C++20 引入的 std::atomic<:shared_ptr>></:shared_ptr> 是为了解决「多个线程安全地交换/读写同一个智能指针变量」的问题,例如:
- 一个全局配置指针,多个线程需要原子地读取最新版本(
load()) - 一个后台线程原子地更新它(
store(new_config))
它**不改变** std::shared_ptr 内部的引用计数行为——内部的 strong_count 仍是原子的,但这个特化类型本身不是用来替代引用计数机制的。
真正难的从来不是“怎么让数字加一”,而是“加一之后,谁负责删内存、什么时候删、删完谁来收尾”。这些边界条件一旦漏掉,程序会在高并发下随机崩溃,而且极难复现。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











