c++oding="utf-8" ?>
不能直接用 std::atomic 做引用计数,因为原子操作仅保证单个操作的原子性,无法保障“计数增减+对象构造/销毁”这一整体语义的原子性,易导致计数泄漏或 double-delete。

为什么不能直接用 std::atomic<int></int> 做引用计数
直接用 std::atomic<int></int> 管理引用计数看似可行,但会漏掉关键语义:引用计数增减必须和对象生命周期严格绑定。比如 ++cnt 后若构造失败或异常抛出,就没人负责回退计数;又或者两个线程同时执行 if (--cnt == 0) delete ptr;,可能 double-delete。原子操作只保操作本身,不保“操作+后续动作”的原子性。
std::shared_ptr 的底层引用计数怎么做到无锁
标准库实现(如 libstdc++ 和 libc++)通常把控制块(control block)和引用计数放在同一块内存里,用单个 std::atomic<size_t></size_t> 存储强引用数,另一个存弱引用数,并配合 std::atomic<void></void> 或指针比较交换(CAS)来避免 ABA 问题。关键点是:所有修改都基于 CAS 循环,且控制块的分配与释放由调用方保证——不是靠引用计数自己管理内存。
- 强计数增减用
fetch_add(1, std::memory_order_relaxed),因为读写不依赖其他变量 - 强计数归零时,用
compare_exchange_strong确保只有一个线程进入delete分支 - 弱计数需在强计数为 0 后继续存在,所以它单独维护,且析构控制块前要等弱计数也归零
手写一个最小可用的无锁引用计数控制块
下面是一个可运行的简化版,只支持强引用、不支持 weak_ptr,重点展示 CAS 如何防止竞态:
struct ControlBlock {
std::atomic<int> strong_count{1};
void* data;
<pre class="brush:php;toolbar:false;">ControlBlock(void* d) : data(d) {}
bool release() {
int prev = strong_count.fetch_sub(1, std::memory_order_acq_rel);
if (prev == 1) {
delete static_cast<int>(data); // 示例:假设 data 是 new int
delete this;
return true;
}
return false;
}
void add_ref() {
strong_count.fetch_add(1, std::memory_order_relaxed);
}</int>
};
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
注意:fetch_sub 返回的是旧值,所以 prev == 1 表示这是最后一次引用;std::memory_order_acq_rel 保证 delete 前所有对该对象的写已全局可见。
容易被忽略的内存序和对齐问题
很多人以为只要用了 std::atomic 就安全,但实际踩坑常发生在内存序和布局上:
-
std::memory_order_relaxed不能用于释放路径——必须用acq_rel或更强序,否则编译器/处理器可能重排delete到计数减之前 - 控制块若含虚函数或继承,需确保其地址对齐满足
alignof(std::max_align_t),否则new出来的内存可能让原子操作未对齐,触发未定义行为(尤其 ARM64) - 不要在控制块里放非 trivial 析构类型——你无法在 CAS 成功后安全调用它的析构函数,除非用 placement new + 显式调用
真正可靠的无锁引用计数极少需要手写;绝大多数场景直接用 std::shared_ptr 更稳妥。手写只在极少数嵌入式或性能敏感路径中出现,且必须配套完整的测试用例覆盖 CAS 失败重试、异常路径、多线程 delete 竞争等边界。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










