必须将引用计数与托管对象分离并置于堆上control block中,使多个shared_ptr共享同一计数;否则各自计数导致释放逻辑错乱。

自己实现一个能用的 std::shared_ptr 简化版,核心不在“多线程安全”或“定制删除器”,而在搞清引用计数怎么和对象生命周期绑定——只要理解 control block 的存在必要性,就踩不到最深的坑。
为什么不能直接在智能指针对象里存引用计数?
因为多个 shared_ptr 可能指向同一块内存,但它们是独立对象。如果每个都存一份计数,++/-- 就完全不同步,析构时根本不知道该不该释放资源。
必须把引用计数(和可能的弱引用计数、自定义删除器)单独拎出来,放在堆上,让所有共享该资源的 shared_ptr 指向同一个 control block。
-
control block通常和托管对象分配在同一块内存(或紧邻),但初版可分开 new,更清晰 - 计数本身要用
int*或std::atomic_int*,普通int不行 - 构造时:先 new control block,再 new 托管对象;析构时:先减强引用计数,为 0 则 delete 托管对象,再减控制块自身引用(为 0 则 delete control block)
shared_ptr 构造与拷贝时,control block 怎么共享?
拷贝构造/赋值不是浅拷贝指针,而是把原 shared_ptr 的 ptr_ 和 ctrl_ 都复制过来,然后对 ctrl_->strong_count 做原子加一。
立即学习“C++免费学习笔记(深入)”;
关键点在于:ptr_ 是裸指针(指向托管对象),ctrl_ 是裸指针(指向 control block),两者解耦。这样即使 ptr_ 是 nullptr(空 shared_ptr),只要 ctrl_ 非空,也能管理计数逻辑。
- 默认构造:两个指针都为
nullptr - 带参构造:
ptr_ = new T{args...},ctrl_ = new control_block{1, 0, [](void* p){ delete static_cast<t>(p); }}</t> - 拷贝构造:仅复制
ptr_和ctrl_,然后++ctrl_->strong_count - 移动构造:把原对象的
ptr_和ctrl_移走,原对象置空,不操作计数
析构时 delete 的顺序和条件是什么?
析构只做两件事:减强引用计数,看是否归零;归零则触发资源释放 + 控制块释放。
注意:弱引用计数(weak_count)不影响托管对象生死,只影响 control block 自身是否可删。哪怕所有 shared_ptr 都没了,只要还有 weak_ptr 持有 ctrl_,control block 就不能 delete —— 否则 weak_ptr::lock() 会访问野指针。
- 析构函数中:若
ctrl_ != nullptr,执行--ctrl_->strong_count - 若
ctrl_->strong_count == 0,调用ctrl_->deleter_(ptr_),再--ctrl_->weak_count - 若此时
ctrl_->weak_count == 0,才delete ctrl_ -
ptr_本身不 delete,它只是个观察者;释放动作由deleter_决定(支持数组、自定义释放等)
最容易漏掉的边界情况:空指针和 reset() 的行为
reset() 不是简单地把 ptr_ 设为 nullptr,它本质是一次“析构 + 构造”:先按当前状态走一遍析构逻辑,再用新值重建(可能为空)。
空 shared_ptr(ptr_ == nullptr && ctrl_ == nullptr)是合法状态,但一旦你用它调用 reset(new T),就必须 new 出新的 control block;而如果 reset(nullptr),则要先完成旧资源清理,再清空自身指针。
- 调用
reset()前,需检查ctrl_是否非空,决定是否走析构流程 -
reset(nullptr)后,ptr_为nullptr,ctrl_应为nullptr(除非 weak_ptr 还活着,但此时本体已放弃管理权) - 重复
reset()同一对象?不会 double-delete,因为每次 reset 都先析构旧状态
control block 的布局和生命周期管理,才是真正区分“能跑”和“真懂”的分水岭。很多人写到一半发现 weak_ptr 拿不到对象,或者多次 reset 后 crash,问题几乎全出在 control block 的 weak_count 更新时机和 delete 条件判断上。

















