文章目录

C++ 智能指针std::weak_ptr解决观察者模式

发布于 2026-04-04 18:52:56 · 浏览 18 次 · 评论 0 条

C++ 智能指针 std::weak_ptr 解决观察者模式

观察者模式是软件开发中最常用的行为型设计模式之一,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都能收到通知并自动更新。然而,在 C++ 中实现观察者模式时,内存管理往往是最棘手的问题。如果处理不当,会导致悬垂指针、内存泄漏或重复删除等严重 bug。std::weak_ptr 正是解决这些问题的利器。


1. 观察者模式的内存困境

1.1 经典实现的问题

在经典的观察者模式实现中,被观察者(Subject)通常持有观察者(Observer)的原始指针。这种设计看似简单,却隐藏着巨大的风险。

#include <iostream>
#include <vector>

// 观察者基类
class Observer {
public:
    virtual void update(int value) = 0;
    virtual ~Observer() = default;
};

// 被观察者
class Subject {
private:
    std::vector<Observer*> observers_;
    int value_;

public:
    void attach(Observer* obs) {
        observers_.push_back(obs);
    }

    void detach(Observer* obs) {
        // 复杂的清理逻辑
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), obs),
            observers_.end()
        );
    }

    void setValue(int v) {
        value_ = v;
        notify();
    }

    void notify() {
        for (auto* obs : observers_) {
            obs->update(value_);  // 悬垂指针风险!
        }
    }
};

考虑这样一个场景:某个 Observer 对象被外部代码销毁,但 Subject 仍然持有指向它的原始指针。当 notify() 被调用时,程序就会访问已经释放的内存,导致未定义行为。更糟糕的是,这种崩溃往往是间歇性的,难以调试。

1.2 常见的错误应对方式

面对这个问题,很多开发者会尝试以下方法,但它们都有明显的缺陷:

方案 问题
依赖外部正确调用 detach() 脆弱的 API 设计,容易遗漏
使用 std::shared_ptr 替代原始指针 可能导致循环引用,两个对象都无法释放
notify() 中捕获异常 无法根本解决问题,只是不让程序立即崩溃

std::weak_ptr 正是为这种场景设计的。它允许你观察一个被 std::shared_ptr 管理对象,而不拥有其生命周期。


2. std::weak_ptr 的核心原理

在深入解决方案之前,需要先理解 std::weak_ptr 的工作原理。

2.1 从 std::shared_ptr 说起

std::shared_ptr 通过引用计数管理对象的生命周期。每当一个新的 shared_ptr 指向对象,引用计数增加;当 shared_ptr 被销毁或离开作用域,引用计数减少。当引用计数归零时,对象被自动销毁。

void sharedPtrDemo() {
    auto ptr = std::make_shared<int>(42);  // 引用计数 = 1

    {
        auto copy = ptr;  // 引用计数 = 2
        std::cout << *copy << std::endl;
    }  // copy 销毁,引用计数 = 1

    // ptr 销毁,引用计数 = 0,对象自动释放
}

2.2 weak_ptr 的特殊性

std::weak_ptr 并不参与引用计数的增减。它就像一个"观察者",只能从 std::shared_ptr 创建,用来检查对象是否仍然存活。

void weakPtrDemo() {
    auto shared = std::make_shared<int>(100);

    std::weak_ptr<int> weak = shared;  // 不增加引用计数

    std::cout << "对象存在: " << weak.expired() << std::endl;  // false

    shared.reset();  // 对象被销毁

    std::cout << "对象已销毁: " << weak.expired() << std::endl;  // true

    // 安全地访问对象
    if (auto locked = weak.lock()) {
        std::cout << *locked << std::endl;
    } else {
        std::cout << "对象已不存在" << std::endl;
    }
}

这段代码的关键在于 lock() 方法:如果对象仍然存在,它返回一个有效的 std::shared_ptr;如果对象已被销毁,它返回空的 shared_ptr。这种非侵入式的检查方式,完美解决了观察者模式中的悬垂指针问题。


3. 用 weak_ptr 重构观察者模式

3.1 改造 Observer 和 Subject

现在将 std::weak_ptr 应用到观察者模式中。核心思路是:Subject 持有 Observer 的 weak_ptr,而不是原始指针或 shared_ptr

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

class Observer : public std::enable_shared_from_this<Observer> {
public:
    virtual ~Observer() = default;

    // 必须在堆上创建才能使用 shared_from_this()
    static std::shared_ptr<Observer> create() {
        return std::shared_ptr<Observer>(new Observer());
    }

    // 让调用者返回 shared_ptr
    std::shared_ptr<Observer> getShared() {
        return shared_from_this();
    }

    virtual void update(int value) = 0;
};

// 具体的观察者实现
class ConcreteObserver : public Observer {
private:
    std::string name_;

public:
    explicit ConcreteObserver(const std::string& name) : name_(name) {}

    void update(int value) override {
        std::cout << "Observer [" << name_ << "] received: " << value << std::endl;
    }
};

这里引入了 std::enable_shared_from_this,这是一个重要的辅助类。它允许对象安全地获取指向自己的 shared_ptr。注意,对象必须通过 shared_ptr 管理,不能在栈上创建,否则调用 shared_from_this() 会抛出异常。

3.2 Subject 的实现

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers_;
    int value_;

public:
    void attach(const std::shared_ptr<Observer>& obs) {
        // 将 shared_ptr 转换为 weak_ptr,不增加引用计数
        observers_.push_back(obs);
    }

    void detach(const std::shared_ptr<Observer>& obs) {
        // 移除所有指向该观察者的 weak_ptr
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [&obs](const std::weak_ptr<Observer>& wp) {
                    auto sp = wp.lock();
                    return !sp || sp.get() == obs.get();
                }),
            observers_.end()
        );
    }

    void setValue(int v) {
        value_ = v;
        notify();
    }

    void notify() {
        // 使用迭代器安全删除已失效的观察者
        for (auto it = observers_.begin(); it != observers_.end(); ) {
            auto obs = it->lock();  // 尝试获取 shared_ptr

            if (obs) {
                obs->update(value_);  // 对象存活,安全调用
                ++it;
            } else {
                // 对象已被销毁,清理无效的 weak_ptr
                it = observers_.erase(it);
            }
        }
    }

    size_t observerCount() const {
        size_t valid = 0;
        for (const auto& wp : observers_) {
            if (!wp.expired()) {
                ++valid;
            }
        }
        return valid;
    }
};

这个实现解决了所有内存管理问题:weak_ptr 不会增加引用计数,观察者的生命周期完全由外部控制。当观察者被销毁后,notify() 会在遍历时自动检测并清理无效的 weak_ptr,整个过程是自清洁的。

3.3 完整使用示例

int main() {
    Subject subject;

    // 创建观察者(必须在堆上)
    auto observer1 = ConcreteObserver::create();
    auto observer2 = ConcreteObserver::create();
    auto observer3 = ConcreteObserver::create();

    // 注册观察者
    subject.attach(observer1);
    subject.attach(observer2);
    subject.attach(observer3);

    std::cout << "初始观察者数量: " << subject.observerCount() << std::endl;

    // 修改值,触发通知
    subject.setValue(10);

    // 销毁 observer2
    observer2.reset();

    std::cout << "observer2 销毁后: " << subject.observerCount() << std::endl;

    // 再次通知,observer2 不会收到(因为已被清理)
    subject.setValue(20);

    // 销毁所有观察者
    observer1.reset();
    observer3.reset();

    std::cout << "所有观察者销毁后: " << subject.observerCount() << std::endl;

    return 0;
}

输出结果:

初始观察者数量: 3
Observer [observer1] received: 10
Observer [observer2] received: 10
Observer [observer3] received: 10
observer2 销毁后: 2
Observer [observer1] received: 20
Observer [observer3] received: 20
所有观察者销毁后: 0

可以看到,observer2 被销毁后,Subject 自动检测到这一点并清理了无效的引用,后续的 notify() 不会对已销毁的对象进行任何操作。


4. 更复杂的场景:嵌套观察者

在某些业务场景中,观察者本身可能也持有 Subject 的引用,形成双向依赖。std::weak_ptr 同样可以优雅地解决这个问题。

class Subject;

class NestedObserver : public Observer {
private:
    std::weak_ptr<Subject> subject_;  // 弱引用,避免循环引用
    std::string name_;

public:
    NestedObserver(const std::string& name, std::shared_ptr<Subject> sub)
        : name_(name), subject_(sub) {}

    void update(int value) override {
        auto sub = subject_.lock();
        if (sub) {
            std::cout << "[" << name_ << "] 观察到变化: " << value;
            std::cout << ",Subject 状态: " << sub->getValue() << std::endl;
        } else {
            std::cout << "[" << name_ << "] 观察到 Subject 已被销毁" << std::endl;
        }
    }
};

class Subject : public std::enable_shared_from_this<Subject> {
private:
    std::vector<std::weak_ptr<Observer>> observers_;
    int value_;

public:
    static std::shared_ptr<Subject> create() {
        return std::shared_ptr<Subject>(new Subject());
    }

    void attach(const std::shared_ptr<Observer>& obs) {
        observers_.push_back(obs);
    }

    void setValue(int v) {
        value_ = v;
        notify();
    }

    int getValue() const { return value_; }

private:
    void notify() {
        for (auto it = observers_.begin(); it != observers_.end(); ) {
            if (auto obs = it->lock()) {
                obs->update(value_);
                ++it;
            } else {
                it = observers_.erase(it);
            }
        }
    }

    Subject() : value_(0) {}  // 私有构造函数
};

这种双向依赖结构如果使用原始指针或 shared_ptr,很容易形成循环引用(A 持有 B 的 shared_ptr,B 又持有 A 的 shared_ptr),导致两个对象都无法被销毁。使用 std::weak_ptr 作为一侧的引用,完美打破了循环,同时保持了观察的功能。


5. 最佳实践与注意事项

5.1 强制使用堆分配

由于 std::enable_shared_from_this 的要求,观察者对象必须在堆上分配,不能在栈上创建。如果在栈上创建,调用 shared_from_this() 会抛出 std::bad_weak_ptr 异常。

// 错误:栈对象
ConcreteObserver obs("bad");
subject.attach(obs);  // 不能工作!

// 正确:堆对象
auto obs = ConcreteObserver::create();
subject.attach(obs);

5.2 detach 的简化

weak_ptr 方案中,detach() 方法不再是必需的。因为 notify() 每次都会自动清理无效的观察者,你可以选择不提供 detach() 接口,简化 API。

class Subject {
public:
    // 只需要 attach,不再需要 detach
    void attach(const std::shared_ptr<Observer>& obs) {
        observers_.push_back(obs);
    }

    void notify() {
        for (auto it = observers_.begin(); it != observers_.end(); ) {
            if (auto obs = it->lock()) {
                obs->update(value_);
                ++it;
            } else {
                it = observers_.erase(it);
            }
        }
    }
};

5.3 性能考量

每次 notify() 调用时遍历整个观察者列表并调用 lock(),会带来一定的性能开销。对于高性能要求的场景,可以采用以下优化策略:

策略 适用场景
延迟清理:只在 attach() 时检查并清理无效引用 观察者数量大,notify() 调用频繁
双重结构:同时维护 weak_ptr 列表和有效计数器 需要快速获取观察者数量
分代清理:每隔 N 次通知才执行完整清理 notify() 极其频繁

一个简单的延迟清理实现:

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers_;
    int value_;
    int notifyCount_ = 0;

    void cleanExpired() {
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [](const std::weak_ptr<Observer>& wp) { return wp.expired(); }),
            observers_.end()
        );
    }

public:
    void attach(const std::shared_ptr<Observer>& obs) {
        observers_.push_back(obs);
        cleanExpired();  // 只在添加新观察者时清理
    }

    void notify() {
        ++notifyCount_;

        for (auto& wp : observers_) {
            if (auto obs = wp.lock()) {
                obs->update(value_);
            }
        }

        // 每 100 次通知清理一次
        if (notifyCount_ % 100 == 0) {
            cleanExpired();
        }
    }
};

6. 总结

std::weak_ptr 为 C++ 观察者模式提供了优雅而安全的内存管理方案。它的核心优势体现在三个方面:

  1. 防止悬垂指针:通过 lock() 检查对象是否存活,确保永远不访问已释放的内存。

  2. 打破循环引用:观察者可以安全地持有 Subject 的引用,而不会导致内存泄漏。

  3. 自清洁机制:无效的观察者引用会在 notify() 过程中自动清理,无需外部干预。

在实际项目中,建议将 std::enable_shared_from_this 作为 Observer 基类的默认继承,并在 API 设计中强制要求通过工厂函数创建观察者对象,以确保内存安全。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文