C++标准库不提供内置引用计数GC,std::shared_ptr是最接近的现成方案,但它不是垃圾回收器,而是确定性销毁的智能指针;其核心约束是仅管理堆对象且无法自动解决循环引用,必须配合std::weak_ptr手动破环。

引用计数 GC 的核心约束:只能管堆对象,且不能处理循环引用
直接说结论:C++ 标准库不提供内置引用计数 GC,std::shared_ptr 是最接近的现成方案,但它不是“垃圾回收器”,而是一个确定性销毁的智能指针。真正自己实现一个带自动回收行为的引用计数机制,必须绕过 delete 的手动调用时机,改用代理对象封装原始指针,并在引用归零时触发销毁——但要注意,这依然无法解决 A 持有 B、B 又持有 A 的循环引用问题,这是引用计数固有的缺陷。
常见错误现象:std::shared_ptr 套娃(比如 shared_ptr<shared_ptr>></shared_ptr>)或自引用(对象内部存自己的 shared_ptr),会导致引用计数永远 >0,内存泄漏;调试时发现对象析构函数没被调用,十有八九是循环引用。
- 只对动态分配对象(
new出来的)有意义,栈对象、全局对象不适用 - 每次拷贝/赋值/传参都需原子增减引用计数,性能开销比裸指针高(尤其在多线程下)
- 必须配合
std::weak_ptr打破循环,否则无解
手写简易引用计数类:关键在构造、拷贝、析构三处重载
如果非要自己写一个最小可行版本(比如教学或嵌入式受限环境),核心是把引用计数和资源绑定在同一块内存里,避免额外查找开销。典型做法是让资源前缀一个计数字段,用 placement new 构造对象。
示例结构:
立即学习“C++免费学习笔记(深入)”;
template<typename T>
class RefCountPtr {
T* ptr_;
size_t* count_;
<pre class="brush:php;toolbar:false;">void release() {
if (count_ && --(*count_) == 0) {
delete ptr_;
delete count_;
}
}public: explicit RefCountPtr(T* p = nullptr) : ptr(p), count(p ? new size_t(1) : nullptr) {}
RefCountPtr(const RefCountPtr& other)
: ptr_(other.ptr_), count_(other.count_) {
if (count_) ++(*count_);
}
RefCountPtr& operator=(const RefCountPtr& other) {
if (this != &other) {
release();
ptr_ = other.ptr_;
count_ = other.count_;
if (count_) ++(*count_);
}
return *this;
}
~RefCountPtr() { release(); }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }};
注意点:
- 没处理线程安全——多线程场景下
++(*count_)非原子,要用std::atomic_size_t - 没支持自定义删除器(如数组、C API 资源),
delete ptr_硬编码了默认行为 - 没做空指针保护,
operator*直接解引用可能 crash
为什么不用 raw pointer + 手动 refcount?现实项目中几乎没人这么干
因为 std::shared_ptr 已经覆盖了绝大多数需求,且经过充分测试和优化。自己实现容易踩的坑远超收益:
- 异常安全:构造过程中抛异常,可能导致计数内存泄露或双重释放
- 类型擦除缺失:无法像
shared_ptr那样支持自定义 deleter 和 aliasing 构造 - 与 STL 容器不兼容:不能直接放进
std::vector<refcountptr>></refcountptr>(缺少移动语义、比较操作符等) - 调试困难:引用计数位置分散,难以追踪谁在持有哪些引用
真实使用场景只剩两种:教学演示(理解原理)、极小内核/游戏引擎底层(为控制内存布局放弃标准库)。
循环引用的实际修复:weak_ptr 不是可选,是必需
只要涉及双向关系(树节点父子、观察者订阅、图结构),就必须用 std::weak_ptr 断开其中一端。它的本质是不增加引用计数的观察者句柄,访问前用 lock() 升级为 shared_ptr,失败说明目标已销毁。
典型错误写法:
struct Node {
std::shared_ptr<Node> parent;
std::vector<std::shared_ptr<Node>> children;
}; // parent + children 形成循环,永远不释放正确写法:
struct Node {
std::weak_ptr<Node> parent; // 不增加父节点引用计数
std::vector<std::shared_ptr<Node>> children;
};容易忽略的细节:
-
weak_ptr::lock()返回的是临时shared_ptr,必须检查是否为空再使用 -
weak_ptr本身不管理生命周期,析构不触发任何动作,也不能解引用 - 频繁调用
lock()有轻微性能成本,但比内存泄漏强得多
真正难的不是写计数逻辑,而是识别哪些引用该用 weak、哪些必须用 shared——这需要对数据流向和生命周期有清晰建模。


















