Observer接口必须声明虚析构函数,否则多态删除会导致析构不完整、内存泄漏或未定义行为;纯接口也需显式声明,编译器不会自动生成。

Observer 接口必须用虚析构函数
不加 virtual ~Observer() = default; 会导致派生类对象通过基类指针删除时析构不完整,内存泄漏或未定义行为。这是 c++ 观察者模式最常被忽略的底层陷阱。
实际场景中,Subject 通常持有 std::vector<:unique_ptr>> 或 std::vector,而后者更常见(避免强制所有权转移)。无论哪种,只要涉及多态删除,就必须有虚析构。
- 用
std::unique_ptr→ 必须虚析构 - 用裸指针
Observer*→ 仍需虚析构(因为delete ptr会调用基类析构) - 若 Observer 是纯接口(无数据成员),也仍需显式声明虚析构,否则编译器不会自动生成
std::function + std::vector 实现松耦合 Observer
比起继承接口,用回调函数注册更轻量、无侵入性,适合事件驱动或脚本化扩展场景。但要注意生命周期管理——std::function 捕获局部变量时极易悬垂。
典型错误是把 Lambda 捕获了栈上对象后存进 Subject 的容器里,稍后调用就崩溃。安全做法是只捕获 this 或智能指针,或确保观察者对象比 Subject 活得久。
立即学习“C++免费学习笔记(深入)”;
class Subject { std::vector> observers_; public: void attach(std::function cb) { observers_.push_back(cb); } void notify(int value) { for (auto& cb : observers_) cb(value); } }; // ✅ 安全:捕获 this(假设 ObserverImpl 生命周期可控) struct ObserverImpl { void onValue(int v) { / ... / } void registerTo(Subject& s) { s.attach([this](int v) { this->onValue(v); }); } };
线程安全不是默认选项
std::vector::push_back 和遍历通知都不是原子操作。多线程下并发 attach() 和 notify() 会触发 data race,甚至导致迭代器失效或容器重分配时崩溃。
常见折中方案是读多写少时用 std::shared_mutex(C++17),或直接加 std::mutex 锁住整个通知流程。但锁粒度太大会降低吞吐,尤其通知链路长时。
- 避免在
notify()中调用可能阻塞或长时间运行的回调 - 若需异步通知,建议把回调转为 post 到线程池,而不是在锁内执行
- 不要在回调里调用
detach()—— 遍历时修改容器是未定义行为;应改用标记+延迟清理(如 erase-remove idiom)
std::weak_ptr 防止循环引用(当用 shared_ptr 管理 Observer)
如果 Subject 和 Observer 都用 std::shared_ptr 互相持有,就会形成循环引用,导致对象永远不析构。解决方案是 Subject 改用 std::weak_ptr 存储,每次通知前先 lock()。
这会带来额外开销(控制块访问 + 引用计数检查),但能彻底解决悬挂指针和泄漏问题。适用于 Observer 生命周期不可控(比如来自插件模块)的场景。
class Subject { std::vector> observers_; public: void attach(std::shared_ptr obs) { observers_.push_back(obs); } void notify() { // 注意:这里要拷贝并过滤已销毁的 observer std::vector> alive; for (auto& w : observers_) { if (auto s = w.lock()) alive.push_back(s); } for (auto& obs : alive) obs->update(); } };
C++ 观察者最难的从来不是结构,而是谁负责生命周期、谁触发销毁、谁保证调用时对象还活着——这些不能靠模式图解决,得靠具体容器语义和所有权约定。