7、weak_ptr详解:弱引用、解决循环引用、expired与lock用法、观察者模式

聊到 weak_ptr,我得先坦白一件事——刚学 C++11 那会儿,我总觉得这玩意儿有点鸡肋。你说它不能直接访问对象,又不像 shared_ptr 那样能自动释放资源,那它到底干嘛用的?

直到我在一个实际项目中,被循环引用坑得焦头烂额……嗯,从那以后,我再也不敢小看 weak_ptr 了。

什么是 weak_ptr?说白了就是“弱引用”

weak_ptr 是 C++11 引入的一种智能指针。它不像 shared_ptr 那样“强”着要拥有对象,而是只“弱弱”地观察着对象。

什么意思呢?

  • 强引用(shared_ptr):对象只要还有强引用,就不会被销毁。
  • 弱引用(weak_ptr):对象是否存活,跟它没关系。它只是“看着”对象,但不阻止对象被销毁。

我个人习惯把 weak_ptr 想象成一个“观察员”。它不参与资源管理,只负责告诉你:“嘿,那个对象还在不在?”

核心要点:weak_ptr 必须从 shared_ptr 构造,不能独立存在。它不会增加引用计数。

循环引用——weak_ptr 的“救火现场”

先说说循环引用是什么。你想想看,两个对象互相用 shared_ptr 指着对方,结果呢?

引用计数永远降不到 0,内存泄漏了。我在项目中遇到过这种场景:一个父节点持有子节点的 shared_ptr,子节点又持有父节点的 shared_ptr。析构函数死活不调用,内存蹭蹭往上涨。

来看个例子:

#include <memory>
#include <iostream>

struct Child;

struct Parent {
    std::shared_ptr<Child> child;
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

struct Child {
    std::shared_ptr<Parent> parent;  // 问题出在这里!
    ~Child() { std::cout << "Child destroyed\n"; }
};

int main() {
    auto p = std::make_shared<Parent>();
    auto c = std::make_shared<Child>();
    p->child = c;
    c->parent = p;  // 互相引用,形成闭环
    // 程序结束,两个对象都不会被销毁!
    return 0;
}

运行结果?什么输出都没有。两个析构函数都没调用。这就是循环引用的典型症状。

警告:循环引用不会导致程序崩溃,但会导致内存泄漏。而且这种泄漏很难排查,因为引用计数看起来一切正常。

用 weak_ptr 打破循环

解决方案很简单:把其中一个 shared_ptr 换成 weak_ptr

我一般建议把“子指向父”的指针改成弱引用。因为父节点通常管理子节点的生命周期,子节点只是“借用”一下父节点的信息。

struct Child {
    std::weak_ptr<Parent> parent;  // 改成弱引用
    ~Child() { std::cout << "Child destroyed\n"; }
};

int main() {
    auto p = std::make_shared<Parent>();
    auto c = std::make_shared<Child>();
    p->child = c;
    c->parent = p;  // 现在没问题了
    // 程序结束,两个对象都能正常销毁
    return 0;
}

输出:

Child destroyed
Parent destroyed

你看,两个析构函数都调用了。问题解决。

expired 和 lock——weak_ptr 的两个核心操作

weak_ptr 不能直接访问对象。你得先问它:“对象还在吗?”

两个方法:

  • expired():检查对象是否已被销毁。返回 true 表示对象没了。
  • lock():尝试获取一个 shared_ptr。如果对象还在,返回有效的 shared_ptr;否则返回空指针。

我习惯这样用:

void use_weak_ptr(std::weak_ptr<int> wp) {
    if (auto sp = wp.lock()) {
        // 对象还在,安全使用
        std::cout << "Value: " << *sp << "\n";
    } else {
        // 对象已销毁
        std::cout << "Object is gone\n";
    }
}

这里有个细节:lock() 是原子操作。它检查对象是否存活,如果存活就增加引用计数,然后返回。这保证了你在使用对象期间,它不会被销毁。

小技巧:不要先调用 expired() 再调用 lock()。因为两者之间对象可能被销毁。直接用 lock() 判断返回值即可,更安全也更简洁。

观察者模式——weak_ptr 的经典应用

观察者模式里,主题(Subject)需要通知观察者(Observer)。但观察者可能随时被销毁。如果用 shared_ptr 持有观察者,就会阻止其销毁,造成资源泄漏。

weak_ptr 就完美了:

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

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

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

    void notify() {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto sp = it->lock()) {
                sp->update();
                ++it;
            } else {
                // 观察者已销毁,移除
                it = observers.erase(it);
            }
        }
    }
};

class ConcreteObserver : public Observer {
    int id;
public:
    ConcreteObserver(int i) : id(i) {}
    void update() override {
        std::cout << "Observer " << id << " notified\n";
    }
};

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();
    obs1.reset();  // 销毁 obs1
    subject.notify();  // 只有 obs2 被通知
    return 0;
}

输出:

Observer 1 notified
Observer 2 notified
Observer 2 notified

你看,obs1 销毁后,主题自动跳过了它。不需要手动解注册,省心多了。

一张图看懂 weak_ptr 的核心逻辑

weak_ptr 核心逻辑 shared_ptr 对象 引用计数 = 1 强引用 实际对象 weak_ptr 观察者 弱引用 操作 • expired() → 检查对象是否存活 • lock() → 获取 shared_ptr(原子操作) 典型应用 • 打破循环引用 • 观察者模式

避坑指南

我曾经在项目里犯过一个低级错误:用 weak_ptr 去管理一个栈上对象。结果 lock() 返回了空指针,排查了半天才发现问题。

记住:weak_ptr 只能配合 shared_ptr 使用。它不能指向栈对象、全局对象或 unique_ptr 管理的对象。

另外,别想着用 weak_ptr 来“提升”性能。它不会减少内存分配,也不会加快访问速度。它的唯一价值就是解决生命周期问题。

特性 shared_ptr weak_ptr
引用计数影响 增加强引用计数 不增加
直接访问对象 可以 不可以(需 lock)
防止对象销毁
典型用途 资源管理 观察、打破循环

嗯,关于 weak_ptr 就聊这么多。它虽然功能简单,但在实际项目中用好了,能省去不少麻烦。下次写代码遇到循环引用,记得试试它。


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