20. 手写一个简易 weak_ptr:与 shared_ptr 控制块的协作、expired 与 lock 的实现
讲完了 shared_ptr 的引用计数,咱们得聊聊它的好搭档——weak_ptr。说实话,我刚学 C++ 那会儿,觉得 weak_ptr 有点多余。既然有了 shared_ptr,为啥还要搞个弱引用?直到我在项目中遇到一个循环引用导致内存泄漏的 bug,排查了两天才找到根因……嗯,从那以后,我再也不敢小看 weak_ptr 了。
20.1 weak_ptr 到底解决了什么问题?
你想想看,两个对象互相持有对方的 shared_ptr,会是什么结果?引用计数永远降不到 0,内存泄漏就这么静悄悄地发生了。weak_ptr 就是来破这个局的——它指向对象,但不增加引用计数。
说白了,weak_ptr 是一个「旁观者」。它知道对象在哪里,但不会阻止对象被销毁。对象活着,它能用;对象死了,它知道。
shared_ptr:拥有对象,控制生命周期weak_ptr:观察对象,不控制生命周期
20.2 控制块里的玄机
要实现 weak_ptr,光靠一个引用计数是不够的。为什么?因为 weak_ptr 需要知道对象是否还活着,但对象可能已经被销毁了。所以控制块里需要两个计数器:
| 计数器 | 含义 | 谁影响它 |
|---|---|---|
| use_count | 强引用计数(shared_ptr 的数量) | shared_ptr 的构造/析构 |
| weak_count | 弱引用计数(weak_ptr 的数量) | weak_ptr 的构造/析构 |
这里有个关键点:控制块本身什么时候释放? 答案是当 use_count 和 weak_count 都变成 0 的时候。对象可以在 use_count 归零时销毁,但控制块得等到 weak_count 也归零才能释放。
20.3 手写一个简易控制块
咱们先实现一个最简版本的控制块。为了清晰,我把它设计成模板类:
template<typename T>
class ControlBlock {
public:
T* ptr; // 管理的对象指针
int shared_count; // 强引用计数
int weak_count; // 弱引用计数
ControlBlock(T* p)
: ptr(p), shared_count(1), weak_count(0) {}
void add_shared() { ++shared_count; }
void release_shared() {
if (--shared_count == 0) {
delete ptr; // 销毁对象
ptr = nullptr;
// 注意:控制块不释放,等 weak_count 归零
}
}
void add_weak() { ++weak_count; }
void release_weak() {
if (--weak_count == 0 && shared_count == 0) {
delete this; // 控制块自我销毁
}
}
int use_count() const { return shared_count; }
bool expired() const { return shared_count == 0; }
};
嗯,这里要注意:release_shared 里只销毁对象,不销毁控制块。控制块的生死由 release_weak 来最终决定。我曾经见过一个新手把 delete this 写在了 release_shared 里,结果 weak_ptr 访问控制块时直接崩溃——血的教训。
20.4 expired 与 lock 的实现
有了控制块,weak_ptr 的核心操作就清晰了:
- expired():检查
shared_count是否为 0 - lock():如果对象还活着,返回一个
shared_ptr;否则返回空
来看代码:
template<typename T>
class WeakPtr {
private:
T* ptr;
ControlBlock<T>* ctrl;
public:
WeakPtr() : ptr(nullptr), ctrl(nullptr) {}
// 从 shared_ptr 构造
WeakPtr(const SharedPtr<T>& sp)
: ptr(sp.ptr), ctrl(sp.ctrl) {
if (ctrl) ctrl->add_weak();
}
// 拷贝构造
WeakPtr(const WeakPtr& other)
: ptr(other.ptr), ctrl(other.ctrl) {
if (ctrl) ctrl->add_weak();
}
// 析构
~WeakPtr() {
if (ctrl) ctrl->release_weak();
}
// 判断是否过期
bool expired() const {
return ctrl == nullptr || ctrl->expired();
}
// 尝试提升为 shared_ptr
SharedPtr<T> lock() const {
if (expired()) {
return SharedPtr<T>(); // 返回空
}
// 对象还活着,构造 shared_ptr
SharedPtr<T> sp;
sp.ptr = ptr;
sp.ctrl = ctrl;
ctrl->add_shared();
return sp;
}
// 获取引用计数(仅用于调试)
int use_count() const {
return ctrl ? ctrl->use_count() : 0;
}
};
lock() 不是原子操作!在多线程环境下,expired() 返回 false 后,另一个线程可能立刻销毁对象。真正的 std::weak_ptr::lock() 是原子操作的,咱们这个简易版为了演示原理做了简化。
20.5 核心逻辑流程图
下面这张图展示了 weak_ptr 与 shared_ptr 通过控制块协作的全过程:
20.6 使用示例与避坑指南
来看看咱们的简易 weak_ptr 怎么用:
// 创建 shared_ptr
SharedPtr<int> sp(new int(42));
WeakPtr<int> wp(sp);
// 检查是否过期
if (!wp.expired()) {
// 提升为 shared_ptr
SharedPtr<int> sp2 = wp.lock();
if (sp2) {
std::cout << *sp2 << std::endl; // 输出 42
}
}
// 释放原始 shared_ptr
sp.reset(); // 对象被销毁
// 此时 weak_ptr 过期
if (wp.expired()) {
std::cout << "对象已销毁" << std::endl;
}
// lock() 返回空
SharedPtr<int> sp3 = wp.lock();
assert(sp3 == nullptr);
weak_ptr,但回调触发时对象已经销毁。当时没检查 expired() 就直接 lock(),结果拿到了一个空 shared_ptr,后续解引用直接崩溃。记住:lock() 之后一定要判空!
20.7 简易版 vs 标准库版
咱们这个简易版跟 std::weak_ptr 比,差距在哪里?
| 特性 | 简易版 | std::weak_ptr |
|---|---|---|
| 线程安全 | ❌ 无 | ✅ 原子操作 |
| 自定义删除器 | ❌ 不支持 | ✅ 支持 |
| 类型擦除 | ❌ 模板直接管理 | ✅ 通过控制块擦除 |
| 内存优化 | ❌ 简单实现 | ✅ 内部分配优化 |
但核心思想是一样的:通过控制块解耦对象的生命周期和引用的生命周期。你理解了咱们这个简易版,再看标准库的实现,会发现万变不离其宗。
weak_ptr。想想能不能用原始指针替代?能不能重新设计所有权关系?weak_ptr 是个好工具,但不是万能的。我见过有人滥用 weak_ptr,结果代码变得又复杂又难维护。
好了,weak_ptr 的核心原理就这些。记住三个关键词:控制块、expired、lock。搞懂了它们,你就掌握了 C++ 智能指针体系中最后一块拼图。
公众号:蓝海资料掘金营,微信deep3321