9. weak_ptr:观察者模式,解决悬空指针

好,咱们今天聊聊 weak_ptr

说实话,很多 C++ 开发者对 shared_ptr 用得挺溜,但一提到 weak_ptr 就有点懵。我刚开始学的时候也一样——「这玩意儿到底干嘛用的?不就是个弱引用吗?」

嗯,它确实是个弱引用。但它的价值,远比你想象的大。

9.1 为什么需要 weak_ptr?

先问你一个问题:shared_ptr 最大的痛点是什么?

你可能会说「循环引用」。没错,这是经典问题。但我想换个角度说——有时候你只是想「看一眼」某个对象,并不想延长它的生命周期

举个例子。你有一个缓存系统,缓存里存着一些大对象。你希望当外部没人用这些对象时,缓存能自动释放它们。但缓存本身又需要知道这些对象是否还活着。

如果用 shared_ptr,缓存会一直持有引用计数,对象永远释放不了。这就是典型的「观察者模式」场景——你只需要观察,不需要拥有。

核心思想:weak_ptr 是一种「不拥有所有权」的智能指针。它指向一个由 shared_ptr 管理的对象,但不会增加引用计数。

9.2 weak_ptr 的基本用法

先看个最简单的例子:

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp;  // 不增加引用计数

    std::cout << "use_count: " << sp.use_count() << std::endl;  // 输出 1

    // 使用前必须 lock()
    if (auto locked = wp.lock()) {
        std::cout << "value: " << *locked << std::endl;
    } else {
        std::cout << "对象已被释放" << std::endl;
    }

    sp.reset();  // 释放 shared_ptr

    if (auto locked = wp.lock()) {
        std::cout << "value: " << *locked << std::endl;
    } else {
        std::cout << "对象已被释放" << std::endl;  // 走这里
    }

    return 0;
}

注意看,weak_ptr 不能直接解引用。你必须先调用 lock(),它会返回一个 shared_ptr。如果对象还活着,你就拿到了有效的指针;如果对象已经死了,返回空指针。

我个人习惯把 lock() 叫做「临时借阅证」——你借到了就能用,借不到说明书已经下架了。

9.3 解决循环引用

这是 weak_ptr 最经典的应用场景。我直接上代码:

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // 用 weak_ptr 打破循环
    int value;

    ~Node() {
        std::cout << "Node " << value << " 被销毁" << std::endl;
    }
};

int main() {
    auto n1 = std::make_shared<Node>();
    auto n2 = std::make_shared<Node>();
    n1->value = 1;
    n2->value = 2;

    n1->next = n2;
    n2->prev = n1;  // 这里不会增加引用计数

    // 函数结束,n1 和 n2 都能正常释放
    return 0;
}

我曾经在一个项目里见过一个双向链表,全部用 shared_ptr 互相指。结果呢?内存泄漏得一塌糊涂。排查了两天才发现是循环引用的问题。从那以后,我定了个规矩:父子关系中,父指向子用 shared_ptr,子指向父用 weak_ptr

注意:weak_ptr 只能从 shared_ptr 或另一个 weak_ptr 构造。你不能直接 new 一个对象给 weak_ptr。

9.4 weak_ptr 的常用操作

操作 说明 示例
lock() 返回一个 shared_ptr,如果对象已释放则返回空 auto sp = wp.lock();
expired() 检查对象是否已被释放 if (wp.expired()) { ... }
use_count() 返回 shared_ptr 的引用计数(不包含 weak_ptr) int cnt = wp.use_count();
reset() 清空 weak_ptr,不再指向任何对象 wp.reset();

这里有个小细节:expired()lock() 不是原子操作。在多线程环境下,你不能先判断 expired()lock(),因为中间可能被其他线程释放。正确的做法是直接 lock(),然后检查返回值。

最佳实践:永远只用 lock() 来获取 weak_ptr 指向的对象。不要先 expired() 再 lock(),那是给自己挖坑。

9.5 观察者模式实战

咱们来写一个完整的观察者模式例子。假设你有一个「主题」和多个「观察者」。主题状态变化时通知所有观察者。但观察者可能随时被销毁。

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

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(int state) = 0;
};

class Subject {
    std::vector<std::weak_ptr<Observer>> observers;
public:
    void addObserver(std::shared_ptr<Observer> obs) {
        observers.push_back(obs);
    }

    void notify(int state) {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto obs = it->lock()) {
                obs->update(state);
                ++it;
            } else {
                // 观察者已销毁,清理掉
                it = observers.erase(it);
            }
        }
    }
};

class ConcreteObserver : public Observer {
    int id;
public:
    ConcreteObserver(int i) : id(i) {}
    void update(int state) override {
        std::cout << "观察者 " << id << " 收到状态: " << state << std::endl;
    }
};

int main() {
    Subject subject;

    auto obs1 = std::make_shared<ConcreteObserver>(1);
    auto obs2 = std::make_shared<ConcreteObserver>(2);

    subject.addObserver(obs1);
    subject.addObserver(obs2);

    subject.notify(100);  // 两个观察者都收到

    obs1.reset();  // 销毁观察者1

    subject.notify(200);  // 只有观察者2收到,观察者1被自动清理

    return 0;
}

你看,用 weak_ptr 实现观察者模式,代码干净又安全。观察者死了,主题不需要手动解注册,下次通知时自动清理。这就是我常说的「弱引用让生命周期管理变得优雅」。

9.6 weak_ptr 的内部机制

你可能会好奇:weak_ptr 怎么知道对象还活着?

其实 shared_ptrweak_ptr 共享一个「控制块」。控制块里有两个计数:

  • 引用计数(use_count):记录 shared_ptr 的数量
  • 弱计数(weak_count):记录 weak_ptr 的数量

当引用计数降到 0 时,对象被销毁。但控制块还活着,直到弱计数也降到 0。

所以 weak_ptr 能通过控制块知道对象是否还活着。这就是为什么 lock() 能返回一个有效的 shared_ptr——如果对象还在,它临时增加引用计数,让你安全使用。

一句话总结:shared_ptr 管对象的生命周期,weak_ptr 管控制块的生命周期。两者配合,天衣无缝。

9.7 知识体系图

下面这张图帮你理清 weak_ptr 的核心逻辑:

weak_ptr 核心知识体系 weak_ptr 为什么需要? 核心用法 应用场景 不增加引用计数 只观察,不拥有 lock() 获取临时 shared_ptr expired() 检查是否存活 打破循环引用 观察者模式 核心原则:用 weak_ptr 观察,用 shared_ptr 拥有

9.8 避坑指南

最后,分享几个我踩过的坑:

  • 不要用 weak_ptr 存储需要长期持有的数据——它随时可能失效。如果你需要保证对象活着,直接用 shared_ptr。
  • lock() 返回的 shared_ptr 用完后记得释放——虽然它不会造成泄漏,但长时间持有会阻止对象被释放。
  • 不要在 weak_ptr 上调用 operator* 或 operator->——它不支持直接解引用。我见过有人硬写 *wp,编译都过不了。
  • 多线程环境下,lock() 是安全的——它内部有原子操作。但如果你先 expired() 再 lock(),那就不是原子的了。

我曾经在一个高并发服务器里,用 weak_ptr 实现了连接池的自动清理。每个连接被 shared_ptr 持有,连接池用 weak_ptr 观察。连接断开后,shared_ptr 释放,weak_ptr 自动失效,池子下次检查时自动清理。整个设计非常优雅,几乎没有内存泄漏的问题。

嗯,这就是 weak_ptr 的精髓。它不抢 ownership,不干涉生命周期,只是在旁边默默观察。需要的时候借给你用一下,不需要了也不纠缠。

说白了,weak_ptr 就是 C++ 智能指针家族里的「观察者」。它让代码更安全,让设计更清晰。用好它,你的 C++ 代码会上一个台阶。


公众号:蓝海资料掘金营,微信deep3321