不能直接用 int 做引用计数,因其非原子性导致多线程下计数丢失,且单线程中易产生悬垂指针;必须用原子类型(如 std::atomic_int)并严格遵循“最后 release 才 delete”的语义,初始化为 1、add_ref 原子自增、release 原子减并判断返回值是否为 1,配合恰当内存序(acq_rel),同时妥善管理计数器生命周期。

为什么不能直接用 int 做引用计数?
裸 int 在多线程环境下会丢计数:比如两个线程同时执行 ++ref_count,底层可能读-改-写三步,中间穿插导致只加了一次。即使单线程,析构时若先减到 0 再 delete,而此时别的地方还拿着旧指针,就成悬垂指针。
所以必须用原子操作,且释放逻辑要严格满足“最后 release 才真正 delete”。
add_ref 只需原子自增,但要注意初始化时机
引用计数必须在对象首次被管理时设为 1(不是 0),否则第一次 add_ref 后是 1,但还没人持有它——语义错乱。
典型做法是在智能指针构造时,把原始指针的计数器(通常和对象一起分配或单独 new)初始化为 1:
counter = new std::atomic_int(1); // 不是 0
之后每次 add_ref 就是:
-
counter->fetch_add(1, std::memory_order_relaxed)—— 大多数场景够用 - 如果计数器本身是全局共享(比如跨线程传递),建议用
std::memory_order_acquire配合后续读,但单纯计数递增不用过度担心顺序
release 必须检查返回值并条件 delete
关键不是“减一”,而是“减完后是不是 0”。fetch_sub 返回的是减之前的值,所以判断逻辑是:
if (counter->fetch_sub(1, std::memory_order_acq_rel) == 1) { /* 最后一个引用 */ }
注意三点:
- 必须用
std::memory_order_acq_rel或更强序:确保之前对对象的所有写操作在 delete 前完成,且 delete 后的内存回收不被重排到判断前 - 不能写成
if (--*counter == 0)—— 这不是原子的,且没内存序保证 - delete 对象后,
counter本身也要 delete(除非它和对象布局在一起,用 placement new 分配)
容易漏掉的边界:空指针与重复 release
release 被调用时指针可能已是 nullptr,或者同一指针被多次 release(比如智能指针被 move 后又析构)。安全做法是:
- 在
release开头加if (!ptr || !counter) return; - 不要依赖外部保证“只 release 一次”,引用计数类自己得防崩
- 如果计数器和对象共分配(如
new char[sizeof(T) + sizeof(std::atomic_int)]),则release中 delete 的是整个块,不是单独delete ptr
最麻烦的其实是计数器生命周期管理——它比所管对象更早分配、更晚销毁,稍不注意就会 double free 或访问已释放的计数器。实际项目中建议直接用 std::shared_ptr,自己写的仅用于理解原理或嵌入式受限环境。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










