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_countweak_count 都变成 0 的时候。对象可以在 use_count 归零时销毁,但控制块得等到 weak_count 也归零才能释放。

我个人习惯:把控制块想象成一个「物业办公室」。住户(shared_ptr)搬走了,房子(对象)可以拆了,但物业档案(控制块)还得留着,直到所有看房的人(weak_ptr)都离开。

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_ptrshared_ptr 通过控制块协作的全过程:

weak_ptr 与 shared_ptr 协作流程图 shared_ptr 强引用,拥有对象 weak_ptr 弱引用,观察对象 控制块 (ControlBlock) shared_count: 强引用计数 weak_count: 弱引用计数 ptr: 指向实际对象 实际对象 (T) 增加 shared_count 增加 weak_count 管理 lock() 提升 expired() 检查 shared_ptr 路径 weak_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