16. weak_ptr:弱引用、lock()、expired()

聊完了 shared_ptr,咱们得面对一个现实问题——循环引用。我记得刚入行那会儿,第一次在项目里大规模用 shared_ptr,自我感觉良好,觉得内存管理终于解放了。结果跑压力测试的时候,内存曲线一路往上飙,最后 OOM 了。排查了半天,发现是几个对象互相持有 shared_ptr,谁也释放不了。

嗯,这时候 weak_ptr 就派上用场了。

弱引用到底是什么?

说白了,weak_ptr 是一个「旁观者」。它指向一个由 shared_ptr 管理的对象,但不增加引用计数。它不拥有这个对象,只是「看着」它。

你想想看,如果 A 和 B 互相持有 shared_ptr,那它们的引用计数永远到不了 0,谁也析构不了。这就是典型的循环引用死锁。用 weak_ptr 打破这个环,让其中一方只观察、不拥有,问题就解决了。

核心区别:

  • shared_ptr:共享所有权,引用计数 +1
  • weak_ptr:无所有权,引用计数不变

lock():从观察到使用

weak_ptr 不能直接访问对象。你想用它指向的对象?得先调用 lock()。这个方法会返回一个 shared_ptr。如果对象还活着,返回有效的 shared_ptr;如果对象已经被释放了,返回空 shared_ptr。

我个人习惯在需要访问 weak_ptr 指向的对象时,先 lock 一下,然后判断是否为空。这样既安全又清晰。

#include <memory>
#include <iostream>

class Observer {
public:
    void observe(std::weak_ptr<int> wp) {
        // 尝试获取 shared_ptr
        if (auto sp = wp.lock()) {
            std::cout << "对象还活着,值为: " << *sp << std::endl;
        } else {
            std::cout << "对象已经被释放了" << std::endl;
        }
    }
};

int main() {
    auto sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp;

    Observer obs;
    obs.observe(wp);  // 输出:对象还活着,值为: 42

    sp.reset();       // 释放 shared_ptr
    obs.observe(wp);  // 输出:对象已经被释放了

    return 0;
}

你看,lock() 返回的 shared_ptr 在 if 语句里临时持有对象,用完了自动释放。这个临时持有很重要——它保证你在访问对象期间,对象不会被其他线程释放掉。

小技巧:lock() 是原子操作。它检查对象是否存活,如果存活就增加引用计数,整个过程是线程安全的。我在多线程项目里经常用这个特性。

expired():先检查再使用?

expired() 用来检查 weak_ptr 指向的对象是否已经被释放。返回 true 表示对象没了,false 表示还活着。

但是——这里有个坑。我曾经在代码里这么写过:

// 错误示范
if (!wp.expired()) {
    auto sp = wp.lock();  // 这里可能已经过期了!
    // 使用 sp...
}

为什么有问题?因为 expired() 检查完到 lock() 之间,可能有另一个线程把 shared_ptr 释放了。expired() 返回 false,但 lock() 却返回了空指针。

所以我建议:直接用 lock(),别先调 expired()。lock() 自己会做原子检查,更安全。

注意:不要用 expired() + lock() 的组合。直接用 lock() 判断返回值就够了。这是我在代码审查里经常指出的问题。

实战:用 weak_ptr 打破循环引用

来个经典例子——父子节点互相引用。

#include <memory>
#include <vector>
#include <iostream>

class Child;

class Parent {
public:
    std::vector<std::shared_ptr<Child>> children;
    ~Parent() { std::cout << "Parent 析构" << std::endl; }
};

class Child {
public:
    std::weak_ptr<Parent> parent;  // 用 weak_ptr 避免循环引用
    ~Child() { std::cout << "Child 析构" << std::endl; }
};

int main() {
    {
        auto p = std::make_shared<Parent>();
        auto c = std::make_shared<Child>();
        p->children.push_back(c);
        c->parent = p;  // weak_ptr 赋值,不增加引用计数
        std::cout << "离开作用域..." << std::endl;
    }
    // 两个对象都能正常析构
    std::cout << "都析构完了" << std::endl;
    return 0;
}

运行结果:

离开作用域...
Parent 析构
Child 析构
都析构完了

如果 Child 里用的是 shared_ptr,Parent 和 Child 就互相牵着不放,谁也析构不了。用 weak_ptr,Parent 释放后,Child 里的 weak_ptr 自动过期,完美。

weak_ptr 的典型应用场景

场景 说明
缓存系统 缓存持有对象的 weak_ptr,对象被其他地方释放后,缓存自动失效
观察者模式 观察者持有被观察者的 weak_ptr,避免观察者阻止被观察者析构
树形结构 子节点用 weak_ptr 指向父节点,父节点用 shared_ptr 持有子节点
线程安全访问 用 weak_ptr 传递对象引用,接收方通过 lock() 安全获取临时所有权

知识体系:weak_ptr 核心逻辑

下面这张图帮你理清 weak_ptr 和 shared_ptr 的关系:

weak_ptr 核心逻辑 shared_ptr 控制块 引用计数: 2 弱引用计数: 1 对象指针 实际对象 (堆上分配) shared_ptr A 拥有所有权 weak_ptr B 无所有权 指向 管理 观察 lock() 流程 1. 检查弱引用计数 > 0 2. 如果对象存活,增加引用计数 3. 返回 shared_ptr(原子操作,线程安全) ⚠ expired() 单独调用不安全 因为检查到使用之间可能被释放 直接用 lock() 判断返回值即可

使用建议

  • 能用 weak_ptr 就别用裸指针——weak_ptr 至少能告诉你对象还在不在,裸指针做不到。
  • lock() 是首选——别搞 expired() + lock() 的组合拳,一步到位最安全。
  • 临时持有——lock() 返回的 shared_ptr 用完了就释放,别长期持有,否则 weak_ptr 的意义就没了。
  • 缓存场景——我习惯在缓存里存 weak_ptr,配合 lock() 判断缓存是否有效,比手动管理生命周期省心多了。

个人经验:我在一个网络库项目里用 weak_ptr 管理连接对象。每个连接由 shared_ptr 持有,各种回调里存 weak_ptr。回调触发时 lock() 一下,如果连接还在就处理,不在了就忽略。这样既避免了循环引用,又防止了野指针。用了好几年,没出过内存问题。

weak_ptr 不是什么复杂的东西,但它解决了一个很实际的问题。你想想看,没有它,循环引用就得靠手动释放或者裸指针来搞,那多容易出 bug。记住一句话:shared_ptr 负责拥有,weak_ptr 负责观察。各司其职,内存安全就有保障。