在大型C++项目里,一个模块的状态变化常常需要触发其他多个模块的响应,比如界面刷新、日志记录、网络同步等。如果让状态持有者直接调用这些响应函数,代码会迅速变成一张错综复杂的调用网,任何功能增减都要改动核心逻辑。观察者模式正是为了解决这类耦合问题而诞生的设计模式,它定义了对象间一对多的依赖关系,让主题在状态改变时自动通知所有依赖者。

一、观察者模式的核心角色与原理
观察者模式包含两种核心角色:主题(Subject)和观察者(Observer)。主题负责维护一个观察者列表,并提供注册、注销以及通知的接口;观察者则定义一个统一的更新接口,具体观察者实现该接口以响应主题通知。这样一来,主题不需要知道观察者的具体类型,只依赖抽象接口,从而实现了编译期和运行期的解耦。
从底层原理看,这种模式本质是对回调机制的规范化。主题内部保存的是指向观察者接口的指针或引用,当事件发生时,主题遍历列表并调用接口方法。由于接口是稳定的,新增观察者不会破坏主题代码,符合开闭原则。在C++中,我们通常用抽象类或结构体来定义观察者接口,用标准容器管理观察者,配合智能指针规避资源泄漏。
1.1 基础接口设计
我们先定义一个最简洁的观察者抽象类,以及一个主题基类。主题基类提供三个公开方法:attach用于订阅,detach用于取消订阅,notify用于广播。注意这里使用std::weak_ptr来管理观察者,防止主题长期持有导致观察者无法释放。
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
// 观察者抽象接口
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& event) = 0;
};
// 主题基类
class Subject {
public:
virtual ~Subject() = default;
void attach(std::weak_ptr<Observer> obs) {
observers.push_back(obs);
}
void detach(std::weak_ptr<Observer> obs) {
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[&](const std::weak_ptr<Observer>& w) {
auto s = w.lock();
auto o = obs.lock();
return !s || !o || s == o;
}),
observers.end());
}
protected:
void notify(const std::string& event) {
for (auto it = observers.begin(); it != observers.end(); ) {
if (auto obs = it->lock()) {
obs->update(event);
++it;
} else {
it = observers.erase(it);
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers;
};
上面的代码展示了如何用weak_ptr避免循环引用。当某个观察者对象被销毁后,其对应的weak_ptr会失效,在notify时我们顺手清理掉失效的条目。这种方式比裸指针安全,也比shared_ptr更轻量,不会阻止观察者正常析构。
二、具体主题与观察者的实现
有了基类之后,我们可以实现一个具体的温度传感器主题,以及控制台显示器和日志器两个具体观察者。温度传感器在数值变化时调用notify,把所有订阅者都通知一遍。
2.1 具体主题类
具体主题继承Subject,并增加业务方法setTemperature。该方法在内部修改状态后,调用受保护的notify把事件字符串发出去。事件内容可以自定义,这里简单传温度值。
class TemperatureSensor : public Subject {
public:
void setTemperature(double temp) {
temperature = temp;
notify("Temperature changed to " + std::to_string(temp));
}
private:
double temperature = 0.0;
};
这种写法把“状态变更”和“通知他人”绑定在同一个业务方法里,但对外部模块而言,它们只需要关心自己是否收到了update调用,完全不用知道主题内部如何存储温度。如果将来要增加湿度传感器,只需再写一个类似的主题类,复用同一套观察者容器机制。
2.2 具体观察者类
下面两个观察者都继承Observer,分别把事件打印到控制台和写入模拟日志。由于它们只依赖抽象接口,彼此之间毫无关联,可以独立测试与部署。
class ConsoleDisplay : public Observer {
public:
void update(const std::string& event) override {
std::cout << "[Console] " << event << std::endl;
}
};
class FileLogger : public Observer {
public:
void update(const std::string& event) override {
std::cout << "[Logger] recorded: " << event << std::endl;
}
};
从代码可以看出,具体观察者没有任何关于主题的成员变量,它们甚至不知道自己被几个主题订阅。这种单向依赖让系统更容易扩展:比如要新增一个短信告警观察者,只需添加新类并在主程序中attach即可,原有类一行都不用改。
三、使用标准库function简化实现
如果项目不想定义那么多接口类,也可以用std::function搭配std::signal风格来做轻量级事件通知。核心思想是把回调函数直接存进容器,主题不再依赖Observer抽象类,进一步降低类型耦合。
3.1 基于function的事件中心
下面实现一个简单的事件分发器,支持任意可调用对象订阅指定事件名。它用map管理事件到处理函数列表的映射,在触发时依次调用。这种写法在小型工具或脚本化逻辑里非常高效。
#include <functional>
#include <unordered_map>
#include <list>
class EventCenter {
public:
using Handler = std::function<void(const std::string&)>;
void subscribe(const std::string& name, Handler h) {
handlers[name].push_back(h);
}
void fire(const std::string& name, const std::string& data) {
if (auto it = handlers.find(name); it != handlers.end()) {
for (auto& h : it->second) {
h(data);
}
}
}
private:
std::unordered_map<std::string, std::list<Handler>> handlers;
};
这种机制牺牲了一定的类型安全(因为Handler签名统一),但换来了极高的灵活性。你可以直接传lambda、成员函数包装后的function,甚至绑定参数的可调用对象。在游戏开发或UI框架里,这类事件中心非常常见。
3.2 两种方案对比
我们用一个表格总结经典接口式与function式的差异,帮助根据实际场景选型。
| 维度 | 抽象接口观察者 | function事件中心 |
|---|---|---|
| 耦合程度 | 主题依赖Observer基类 | 主题不依赖具体类型 |
| 扩展性 | 新增观察者需写新类 | 直接传lambda即可 |
| 类型安全 | 编译期接口约束强 | 运行时签名统一 |
| 适用规模 | 中大型长期维护项目 | 快速原型或轻量模块 |
从维护角度看,接口式更适合边界清晰、观察者数量固定且需要强制实现规范的系统;function式则适合事件种类多、处理逻辑零散的场景。两者都达成了事件通知与解耦的目标,只是抽象层级不同。
四、生命周期与线程安全注意点
在真实C++服务中,观察者和主题可能分属不同线程。如果主题在notify时观察者正在析构,就可能发生竞态。前面用weak_ptr已经解决了部分悬空指针问题,但在多线程下仍需加锁保护容器。
4.1 加锁的线程安全主题
我们可以给Subject的容器操作加上std::mutex,确保attach、detach和notify不会同时修改或遍历同一份数据。以下示例仅展示核心改动。
#include <mutex>
class SafeSubject : public Subject {
protected:
void notify(const std::string& event) {
std::lock_guard<std::mutex> lock(mtx);
// 原notify逻辑,注意lock期间不要调用外部可能回注册的长函数
Subject::notify(event);
}
private:
std::mutex mtx;
};
需要注意的是,如果在observer的update内部又调用了主题的attach或detach,就可能造成死锁或迭代器失效。因此良好的实践是:update里只做轻量响应,重订阅操作放到主题外部的主线程或专用队列里处理。
4.2 避免通知过程中的自我修改
有些开发者喜欢在update里直接detach自己,这会让主题的遍历过程修改容器。推荐做法是让观察者标记“待注销”,主题在notify结束后统一清理,或者改用copy-on-write方式在通知前拷贝一份观察者列表。
设计观察者模式时,永远假设观察者是不可信的:它可能很慢、可能抛异常、可能在回调中修改主题。主题要做的是隔离这些风险,而不是依赖观察者遵守约定。
通过合理的容器选择、智能指针与锁策略,C++里的观察者模式既能保持灵活解耦,也能在复杂环境下稳定运行。无论是传统接口方案还是现代function方案,核心都是把“谁关心变化”和“变化如何发生”彻底分开。
observer_patternC++_eventdecoupling修改时间:2026-08-10 07:03:46