应手写引用计数而非用 std::shared_ptr,因需控制资源生命周期语义(如专用API、跨DLL、无STL环境),核心是计数器与资源分离且线程安全,推荐用 std::atomic 实现,初始化为1,add_ref 用 relaxed,release_ref 用 acq_rel 并检查返回值是否为1以触发释放。

为什么不用 std::shared_ptr 而要手写引用计数?
因为你要控制资源生命周期的语义——比如资源创建和销毁必须走特定 API(如 glGenTextures/glDeleteTextures),或资源需跨 DLL 边界、不能依赖 STL ABI 兼容性,又或者只是嵌入式环境压根没 STL。这时候手写轻量引用计数不是“造轮子”,而是绕过抽象泄漏的必要妥协。
核心原则:计数器和资源分离,避免把计数逻辑侵入资源类型本身;计数器本身必须是线程安全的(哪怕只用原子操作)。
std::atomic<int></int> 是最简可行计数器
别用 int + 手动加锁——性能差、易漏锁、难验证。用 std::atomic<int></int> 足够覆盖绝大多数单进程场景,且无额外依赖。
- 初始化计数器为
1(首次持有即“已引用”) -
add_ref()用fetch_add(1, std::memory_order_relaxed)—— 引用增加不依赖顺序 -
release_ref()用fetch_sub(1, std::memory_order_acq_rel)—— 减到 0 的瞬间需要同步语义 - 检查返回值是否为
1:只有减完等于 1,才说明这是最后一次引用,该释放资源
示例:
立即学习“C++免费学习笔记(深入)”;
class TextureHandle {
GLuint id_;
std::atomic<int>* ref_count_;
<p>public:
TextureHandle(GLuint id) : id_(id), ref<em>count</em>(new std::atomic<int>(1)) {}</int></p><pre class="brush:php;toolbar:false;">void add_ref() { ref_count_->fetch_add(1, std::memory_order_relaxed); }
bool release_ref() {
if (ref_count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {
glDeleteTextures(1, &id_);
delete ref_count_;
return true;
}
return false;
}};
裸指针 + 外部计数器的典型误用坑
常见错误是把计数器放在资源对象内部(比如 Texture 类里塞一个 std::atomic<int></int>),然后传 Texture* 到各处——这会导致:资源析构后指针仍可能被误用,而计数器早已销毁,fetch_sub 变成未定义行为。
- 计数器生命周期必须严格长于所有引用它的指针
- 推荐方案:资源句柄(如
TextureHandle)持有一份std::atomic<int>*</int>,而非内联计数器 - 禁止裸指针直接指向带内联计数器的资源对象
- 如果资源本身不可拷贝,确保
TextureHandle显式删除拷贝构造/赋值,只允许移动或手动add_ref
跨线程传递时,std::memory_order 选错会静默崩溃
在多线程频繁增减引用的场景下,std::memory_order_relaxed 对 add_ref 安全,但 release_ref 的减法必须用 acq_rel 或更强语义——否则其他线程可能看到资源已被 glDeleteTextures,却还在读写其纹理 ID。
- 不要为了“看起来快”在
release_ref里用relaxed - 如果资源释放涉及复杂清理(如回调、日志、子资源递归释放),建议在
fetch_sub后立即加一道std::atomic_thread_fence(std::memory_order_acquire)防重排 - Windows 上若目标是 XP 兼容,
std::atomic可能退化为锁实现,此时需确认性能可接受
真正的难点不在写计数逻辑,而在于界定“谁负责分配计数器内存”“谁触发最终释放”“错误路径下计数器是否一定被清理”。这几个点一旦模糊,调试时大概率遇到 double-free 或 use-after-free,而且只在压力测试下复现。









