当前位置: 首页 > news >正文

C++监听者模式:从原理到现代实现与实战应用

1. 项目概述:为什么我们需要监听者模式?

在C++项目里,尤其是那些涉及用户界面、游戏逻辑或者分布式系统的场景,我们经常会遇到一个经典问题:一个对象的状态变化了,其他一堆对象需要立刻知道并做出反应。比如,游戏里一个Boss的血量降到零,UI上的血条要消失、任务列表要更新、背景音乐要切换、成就系统要弹窗。如果让Boss对象去直接调用UI、音乐、成就系统这一大堆模块的接口,代码会立刻变得像一团乱麻,Boss类会严重依赖所有它需要通知的模块,牵一发而动全身,维护和扩展简直是噩梦。

监听者模式(Observer Pattern),也叫观察者模式,就是专门用来优雅地解决这类“一对多”依赖关系的利器。它的核心思想是“解耦”:让状态发生变化的对象(称为“主题”或“发布者”)和那些关心状态变化的对象(称为“监听者”或“订阅者”)之间不要直接认识。主题只负责维护一个监听者列表,并在自身状态改变时,像广播一样通知列表里的所有监听者:“我变了!”。至于监听者们各自要做什么,主题完全不用关心。

这种模式在C++中应用极其广泛,从MFC、Qt的信号槽机制,到游戏引擎中的事件系统,再到现代C++标准库中的std::functionstd::bind组合,其背后都有监听者模式的影子。掌握它,你就能写出更灵活、更健壮、更容易应对需求变化的C++代码。接下来,我们就从最基础的原理开始,一步步拆解如何在C++中实现一个工业级的监听者模式,并探讨几个接地气的应用案例。

2. 监听者模式的核心原理与角色拆解

监听者模式的结构非常清晰,主要包含四个核心角色。理解每个角色的职责,是灵活运用和变通该模式的基础。

2.1 主题(Subject) / 发布者(Publisher)

这是整个模式的中心。它拥有其他对象感兴趣的状态。当这个状态发生变化时,它需要通知所有登记在册的监听者。它的核心职责有三点:

  1. 维护监听者列表:提供一个容器(如std::vectorstd::list)来存储所有监听者的引用或指针。
  2. 提供订阅接口:对外暴露AttachSubscribe方法,允许其他监听者对象将自己注册到列表中。
  3. 提供取消订阅接口:对外暴露DetachUnsubscribe方法,允许监听者从列表中移除自己。
  4. 通知监听者:当内部状态改变时,调用NotifyPublish方法,遍历监听者列表,并调用每个监听者预先约定好的更新方法。

注意:一个设计良好的主题应该只依赖于监听者的抽象接口(基类),而不是具体的监听者实现类。这是实现解耦的关键,符合“依赖倒置”原则。

2.2 监听者(Observer) / 订阅者(Subscriber)

这是对主题状态变化感兴趣的对象。它需要实现一个统一的接口(通常是一个包含Update方法的抽象基类)。当主题发出通知时,所有监听者的Update方法会被调用。监听者在这个方法内部,可以获取主题的新状态(主题通常会将新数据作为参数传递),然后执行自己的业务逻辑。

2.3 具体主题(Concrete Subject)

这是主题接口的具体实现类。它除了实现主题的基本管理功能外,还持有实际的状态数据。当这些状态数据通过SetState之类的方法被修改时,它应该在修改后(或修改前,取决于业务)主动触发Notify操作。

2.4 具体监听者(Concrete Observer)

这是监听者接口的具体实现类。它实现了Update方法,定义了当主题状态变化时自己应该做出的具体反应。一个具体监听者通常会持有对其所关注的具体主题的引用(指针或引用),以便在Update方法中能向主题查询更详细的信息。

它们之间的关系,可以用一个简单的比喻来理解:主题就像一个新闻出版社,监听者们就像订阅了报纸的读者。出版社(主题)维护着一个订阅名单(监听者列表)。每当有新报纸出版(状态改变),出版社就按照名单把报纸寄给每一位读者(调用Update)。读者收到报纸后,是仔细阅读、剪下优惠券还是直接扔掉(执行各自的业务逻辑),出版社完全不管。

3. 基础实现:从零开始手搓一个监听者模式

理解了原理,我们动手实现一个最经典、最易懂的版本。这个版本会清晰地展示上述四个角色是如何协作的。

3.1 定义抽象接口

首先,我们定义监听者和主题的抽象基类。使用纯虚函数来强制子类实现。

// Observer.h #ifndef OBSERVER_H #define OBSERVER_H #include <string> // 前向声明,避免头文件循环包含 class Subject; // 抽象观察者接口 class Observer { public: virtual ~Observer() = default; // 更新接口,当主题状态改变时被调用。 // 参数通常为主题对象的引用或状态信息。 virtual void Update(const Subject& subject) = 0; }; #endif // OBSERVER_H
// Subject.h #ifndef SUBJECT_H #define SUBJECT_H #include <list> #include <memory> // 用于std::weak_ptr,后面会讲 class Observer; // 抽象主题接口 class Subject { public: virtual ~Subject() = default; // 订阅(添加观察者) virtual void Attach(Observer* observer) = 0; // 取消订阅(移除观察者) virtual void Detach(Observer* observer) = 0; // 通知所有观察者 virtual void Notify() = 0; protected: // 观察者列表。使用指针存储,注意生命周期管理! std::list<Observer*> observers_; }; #endif // SUBJECT_H

3.2 实现具体主题

我们实现一个简单的“天气数据”主题,它负责收集温度、湿度、气压数据,并在数据更新时通知所有监听者。

// WeatherData.h #ifndef WEATHERDATA_H #define WEATHERDATA_H #include "Subject.h" #include <string> // 具体主题:天气数据站 class WeatherData : public Subject { public: WeatherData() : temperature_(0.0f), humidity_(0.0f), pressure_(1013.25f) {} // 设置新的测量值,并触发通知 void SetMeasurements(float temperature, float humidity, float pressure) { temperature_ = temperature; humidity_ = humidity; pressure_ = pressure; MeasurementsChanged(); // 数据改变,通知观察者 } // 提供给观察者获取数据的接口(常量成员函数,线程安全考虑) float GetTemperature() const { return temperature_; } float GetHumidity() const { return humidity_; } float GetPressure() const { return pressure_; } private: // 当测量值更新时调用的私有方法 void MeasurementsChanged() { Notify(); // 调用基类的通知方法 } // 实现基类的纯虚函数 void Attach(Observer* observer) override { observers_.push_back(observer); } void Detach(Observer* observer) override { observers_.remove(observer); } void Notify() override { for (auto* obs : observers_) { if (obs) { obs->Update(*this); // 将自身(WeatherData)传递给观察者 } } } // 天气数据状态 float temperature_; float humidity_; float pressure_; }; #endif // WEATHERDATA_H

3.3 实现具体监听者

我们创建两个不同的显示设备作为监听者:一个当前状况显示,一个统计数据显示。

// CurrentConditionsDisplay.h #ifndef CURRENT_CONDITIONS_DISPLAY_H #define CURRENT_CONDITIONS_DISPLAY_H #include "Observer.h" #include "WeatherData.h" #include <iostream> class CurrentConditionsDisplay : public Observer { public: // 在构造时订阅主题 explicit CurrentConditionsDisplay(WeatherData& weatherData) : weatherData_(weatherData), temperature_(0.0f), humidity_(0.0f) { weatherData_.Attach(this); } ~CurrentConditionsDisplay() override { // 析构时取消订阅,避免悬空指针 weatherData_.Detach(this); } // Observer接口实现 void Update(const Subject& subject) override { // 安全地将Subject向下转型为WeatherData // 注意:这里假设Update只被WeatherData调用,实际项目中需要更安全的类型检查 const WeatherData* wd = dynamic_cast<const WeatherData*>(&subject); if (wd) { temperature_ = wd->GetTemperature(); humidity_ = wd->GetHumidity(); Display(); } } void Display() const { std::cout << "[当前状况] 温度: " << temperature_ << "°C, 湿度: " << humidity_ << "%" << std::endl; } private: WeatherData& weatherData_; // 持有主题的引用,用于取消订阅 float temperature_; float humidity_; };
// StatisticsDisplay.h #ifndef STATISTICS_DISPLAY_H #define STATISTICS_DISPLAY_H #include "Observer.h" #include "WeatherData.h" #include <iostream> #include <vector> #include <algorithm> #include <numeric> class StatisticsDisplay : public Observer { public: explicit StatisticsDisplay(WeatherData& weatherData) : weatherData_(weatherData) { weatherData_.Attach(this); tempReadings_.reserve(10); // 预分配空间 } ~StatisticsDisplay() override { weatherData_.Detach(this); } void Update(const Subject& subject) override { const WeatherData* wd = dynamic_cast<const WeatherData*>(&subject); if (wd) { float temp = wd->GetTemperature(); tempReadings_.push_back(temp); // 只保留最近10次读数 if (tempReadings_.size() > 10) { tempReadings_.erase(tempReadings_.begin()); } Display(); } } void Display() const { if (tempReadings_.empty()) { std::cout << "[统计数据] 暂无数据" << std::endl; return; } float maxTemp = *std::max_element(tempReadings_.begin(), tempReadings_.end()); float minTemp = *std::min_element(tempReadings_.begin(), tempReadings_.end()); float avgTemp = std::accumulate(tempReadings_.begin(), tempReadings_.end(), 0.0f) / tempReadings_.size(); std::cout << "[统计数据] 平均/最高/最低温度: " << avgTemp << "/" << maxTemp << "/" << minTemp << "°C" << std::endl; } private: WeatherData& weatherData_; std::vector<float> tempReadings_; };

3.4 客户端代码与运行演示

最后,我们写一个简单的main函数来演示整个流程。

// main.cpp #include "WeatherData.h" #include "CurrentConditionsDisplay.h" #include "StatisticsDisplay.h" int main() { // 1. 创建主题(天气数据站) WeatherData weatherStation; // 2. 创建观察者(显示设备),并在构造时自动订阅 CurrentConditionsDisplay currentDisplay(weatherStation); StatisticsDisplay statsDisplay(weatherStation); std::cout << "=== 模拟天气数据更新 ===" << std::endl; // 3. 主题状态改变,自动通知所有观察者 weatherStation.SetMeasurements(25.0f, 65.0f, 1015.0f); std::cout << std::endl; weatherStation.SetMeasurements(26.5f, 70.0f, 1012.0f); std::cout << std::endl; weatherStation.SetMeasurements(23.0f, 90.0f, 1018.0f); std::cout << std::endl; // 4. 观察者会在析构时自动取消订阅 return 0; }

输出结果:

=== 模拟天气数据更新 === [当前状况] 温度: 25°C, 湿度: 65% [统计数据] 平均/最高/最低温度: 25/25/25°C [当前状况] 温度: 26.5°C, 湿度: 70% [统计数据] 平均/最高/最低温度: 25.75/26.5/25°C [当前状况] 温度: 23°C, 湿度: 90% [统计数据] 平均/最高/最低温度: 24.8333/26.5/23°C

这个基础实现清晰地展示了模式的工作流程。但把它用到真实项目中,你会发现不少坑。接下来,我们就深入探讨这些“坑”以及如何用现代C++特性来填平它们。

4. 进阶实现:现代C++下的健壮性优化

上面的基础版本在教学上很清晰,但在生产环境中直接使用会面临内存安全、线程安全、灵活性不足等问题。下面我们逐一优化。

4.1 解决“悬空指针”与内存管理问题

在基础实现中,主题用原始指针Observer*保存观察者。这带来了一个致命问题:如果观察者对象先于主题被销毁(比如一个局部显示的窗口被关闭),主题的列表里就留下了一个“悬空指针”(Dangling Pointer)。下次主题调用Notify()时,对悬空指针解引用会导致未定义行为,通常是程序崩溃。

解决方案1:使用std::weak_ptrstd::shared_ptr这是现代C++推荐的做法。主题持有观察者的std::weak_ptr,观察者自身由std::shared_ptr管理生命周期。weak_ptr不会增加引用计数,因此不会阻止观察者被销毁。在通知前,主题需要尝试将weak_ptr提升(lock)为shared_ptr,如果提升成功,说明观察者还活着,可以安全调用。

// SubjectModern.h #include <memory> #include <vector> class ObserverModern; // 前向声明 class SubjectModern { public: virtual ~SubjectModern() = default; virtual void Attach(std::weak_ptr<ObserverModern> observer) = 0; virtual void Detach(std::weak_ptr<ObserverModern> observer) = 0; // 移除需要小心,比较weak_ptr可能不可靠 virtual void Notify() = 0; protected: std::vector<std::weak_ptr<ObserverModern>> observers_; }; // 具体主题实现Notify时 void WeatherDataModern::Notify() { // 使用“擦除-移除”惯用法安全地清理过期指针并通知 auto it = observers_.begin(); while (it != observers_.end()) { if (auto sp = it->lock()) { // 尝试提升为shared_ptr sp->Update(*this); ++it; } else { // weak_ptr已过期,观察者对象已销毁,从列表中移除 it = observers_.erase(it); } } }

实操心得:直接比较weak_ptrDetach是不可靠的,因为weak_ptr不管理对象。更常见的做法是,让观察者在析构时主动调用主题的Detach,或者像上面在Notify时自动清理过期项。另一种思路是,让主题持有观察者的std::shared_ptr,但这会导致循环引用(观察者也可能持有主题的shared_ptr),同样需要weak_ptr来打破。

解决方案2:使用唯一标识符(Token)管理为每个观察者分配一个唯一ID(如整数或UUID)。主题用std::unordered_map<ID, Observer*>来存储。观察者取消订阅或主题通知时,通过ID来操作。这避免了直接比较指针,但依然无法完全解决悬空指针问题,需要在Notify时检查指针有效性或结合智能指针。

using ObserverID = uint64_t; std::unordered_map<ObserverID, std::weak_ptr<ObserverModern>> observerMap_;

4.2 提升灵活性:使用std::function与信号槽

传统的基于继承的监听者模式要求所有监听者都必须继承自同一个Observer基类,这在某些场景下限制太大。比如,你想监听一个按钮点击事件的,可能是一个普通的成员函数、一个lambda表达式、一个自由函数,或者另一个对象的成员函数。

C++11的std::functionstd::bind(或lambda)提供了完美的解决方案。我们可以实现一个更通用的“信号”(Signal)类,它不关心监听者是谁,只关心调用签名。

// Signal.h - 一个简单的信号槽实现 #include <functional> #include <vector> #include <memory> template <typename... Args> class Signal { public: using Slot = std::function<void(Args...)>; // 槽函数类型 using Connection = std::shared_ptr<void>; // 连接令牌,用于管理槽的生命周期 // 连接一个槽函数,返回一个连接令牌 Connection Connect(Slot slot) { slots_.push_back(slot); // 返回一个令牌,当令牌销毁时,理论上可以断开连接(这里简化了) // 更复杂的实现会存储一个weak_ptr到slot,并通过令牌控制。 return std::make_shared<int>(0); // 简化版令牌 } // 发射信号,调用所有连接的槽 void Emit(Args... args) { // 在遍历过程中,槽函数可能会修改slots_向量(比如断开自身), // 因此需要先复制一份。对于性能敏感场景,需要更精细的锁或策略。 auto slotsCopy = slots_; for (auto& slot : slotsCopy) { if (slot) { slot(args...); } } } // 断开所有连接(简化) void DisconnectAll() { slots_.clear(); } private: std::vector<Slot> slots_; }; // 使用示例 #include <iostream> class Button { public: Signal<> clicked; // 一个无参数信号 void SimulateClick() { std::cout << "Button clicked!" << std::endl; clicked.Emit(); // 发射信号 } }; int main() { Button btn; // 连接一个lambda表达式 auto conn1 = btn.clicked.Connect([]() { std::cout << "Lambda: Handling click!" << std::endl; }); // 连接一个自由函数 auto conn2 = btn.clicked.Connect(&OnButtonClicked); // 连接一个对象的成员函数 Logger logger; auto conn3 = btn.clicked.Connect(std::bind(&Logger::LogEvent, &logger)); btn.SimulateClick(); // 输出: // Button clicked! // Lambda: Handling click! // Free function: Click handled. // [Logger] Event: Button clicked return 0; }

这种基于std::function的实现,其灵活性和Qt的信号槽、Boost.Signals2库非常接近,是现代C++事件系统的基石。

4.3 线程安全考虑

如果主题和监听者可能存在于不同的线程中(例如,一个后台工作线程更新数据,需要通知UI线程更新界面),那么AttachDetachNotify操作都必须是线程安全的。

最简单的做法:使用std::mutex保护共享数据。

#include <mutex> #include <shared_mutex> // C++17 对于读多写少场景更高效 class ThreadSafeSubject : public Subject { public: void Attach(Observer* observer) override { std::lock_guard<std::mutex> lock(mutex_); observers_.push_back(observer); } void Detach(Observer* observer) override { std::lock_guard<std::mutex> lock(mutex_); observers_.remove(observer); // std::list的remove是O(n) } void Notify() override { std::lock_guard<std::mutex> lock(mutex_); // 通知过程中列表不能变 for (auto* obs : observers_) { if (obs) { // 注意:obs->Update()的调用是在锁内进行的! // 如果Update()很耗时或可能再次尝试获取同一把锁(导致死锁),这会成为性能瓶颈。 obs->Update(*this); } } } private: mutable std::mutex mutex_; // mutable允许在const成员函数中加锁 std::list<Observer*> observers_; };

重要警告:在锁内调用未知的用户代码(obs->Update())是极其危险的。如果Update函数内部又调用了AttachDetach,会导致死锁(因为它在等待已经被当前线程持有的锁)。更安全的做法是,在锁的保护下,将需要通知的观察者列表复制出来,然后在锁外进行通知。

void Notify() override { std::list<Observer*> observersCopy; { std::lock_guard<std::mutex> lock(mutex_); observersCopy = observers_; // 复制列表 } // 锁在这里释放 for (auto* obs : observersCopy) { if (obs) { obs->Update(*this); // 在锁外调用,安全 } } }

5. 典型应用案例深度剖析

理解了原理和实现,我们来看看监听者模式在几个经典场景中是如何大显身手的。

5.1 案例一:GUI框架中的事件处理(以简化模型为例)

几乎所有GUI框架(MFC, Qt, wxWidgets, .NET WinForms)的核心都是事件驱动,而这正是监听者模式的用武之地。控件(如按钮)是主题,事件处理函数(或监听器对象)是观察者。

// 一个简化的GUI按钮实现 class Button { Signal<> onClicked; // 点击事件信号 Signal<int, int> onMouseMove; // 鼠标移动事件信号,带坐标参数 public: void Paint() { /* 绘制按钮 */ } void HandleMouseDown(int x, int y) { if (IsPointInsideButton(x, y)) { // ... 处理鼠标按下逻辑 onClicked.Emit(); // 触发点击事件 } } void HandleMouseMove(int x, int y) { onMouseMove.Emit(x, y); } }; // 使用 Button myButton; myButton.onClicked.Connect([]() { std::cout << "Button was clicked! Performing action..." << std::endl; // 例如,打开一个新窗口或提交表单 }); // 另一个窗口想监听这个按钮 class SettingsWindow { public: void OpenSettings() { /* ... */ } }; SettingsWindow settingsWin; myButton.onClicked.Connect(std::bind(&SettingsWindow::OpenSettings, &settingsWin));

优势Button类完全不知道谁会响应它的点击。SettingsWindow也不需要知道按钮的内部实现。两者通过信号槽解耦,极大地提高了代码的模块化和可复用性。

5.2 案例二:游戏开发中的成就系统与事件总线

在游戏中,成就系统是监听者模式的绝佳案例。玩家杀死一个怪物、拾取一个宝物、到达一个地点,这些事件都会触发成就检查。

// 游戏内事件类型 enum class GameEventType { EnemyKilled, ItemCollected, LevelCompleted, PlayerDied }; struct GameEvent { GameEventType type; std::any data; // 可以使用std::any或自定义联合体来存储事件数据 }; // 全局事件总线(一个单例主题) class EventBus : public SubjectModern { public: static EventBus& GetInstance() { static EventBus instance; return instance; } void PublishEvent(const GameEvent& event) { lastEvent_ = event; Notify(); } const GameEvent& GetLastEvent() const { return lastEvent_; } private: EventBus() = default; GameEvent lastEvent_; }; // 具体成就监听者 class AchievementObserver : public ObserverModern { std::set<std::string> unlockedAchievements_; public: void Update(const Subject& subject) override { const EventBus* bus = dynamic_cast<const EventBus*>(&subject); if (!bus) return; const GameEvent& event = bus->GetLastEvent(); switch (event.type) { case GameEventType::EnemyKilled: { // 假设data里存了敌人ID // int enemyId = std::any_cast<int>(event.data); // if (enemyId == BOSS_ID && !unlockedAchievements_.count("BOSS_SLAYER")) { // UnlockAchievement("BOSS_SLAYER"); // } break; } case GameEventType::ItemCollected: { // 检查是否集齐所有物品 break; } // ... 处理其他事件 } } void UnlockAchievement(const std::string& id) { unlockedAchievements_.insert(id); std::cout << "成就解锁: " << id << std::endl; } }; // 在游戏逻辑中 void GameLogic::OnBossDefeated() { // ... 处理Boss死亡逻辑 GameEvent event{GameEventType::EnemyKilled, BOSS_ID}; EventBus::GetInstance().PublishEvent(event); // 发布事件,成就系统会自动响应 }

这种基于事件总线的设计,使得游戏逻辑(发布者)和成就系统、统计系统、音效系统(订阅者)完全解耦。添加新的成就或监听者,完全不需要修改现有的游戏逻辑代码。

5.3 案例三:数据监控与实时仪表盘

假设你有一个后台服务,不断从传感器或日志中收集性能指标(CPU、内存、网络流量)。你需要一个实时仪表盘来可视化这些数据。监听者模式可以轻松实现。

class MetricsCollector : public Subject { std::map<std::string, double> currentMetrics_; public: void UpdateMetric(const std::string& name, double value) { currentMetrics_[name] = value; Notify(); // 数据更新,通知所有仪表盘 } const std::map<std::string, double>& GetMetrics() const { return currentMetrics_; } }; // 一个将数据输出到终端的简单仪表盘 class ConsoleDashboard : public Observer { public: void Update(const Subject& subject) override { const MetricsCollector* mc = dynamic_cast<const MetricsCollector*>(&subject); if (mc) { system("cls"); // 清屏,Windows系统。Linux/Mac用 "clear" std::cout << "===== 实时监控仪表盘 =====" << std::endl; for (const auto& [name, value] : mc->GetMetrics()) { std::cout << name << ": " << value << std::endl; } } } }; // 一个将数据写入文件的监听者 class FileLogger : public Observer { std::ofstream logFile_; public: FileLogger(const std::string& filename) : logFile_(filename, std::ios::app) {} void Update(const Subject& subject) override { const MetricsCollector* mc = dynamic_cast<const MetricsCollector*>(&subject); if (mc) { auto t = std::time(nullptr); logFile_ << std::put_time(std::localtime(&t), "%F %T") << ", "; for (const auto& [name, value] : mc->GetMetrics()) { logFile_ << name << "=" << value << " "; } logFile_ << std::endl; } } };

在这个案例中,MetricsCollector是主题,ConsoleDashboardFileLogger是监听者。数据源只需要向收集器推送数据,而无需关心有多少个、什么类型的仪表盘在消费数据。你可以随时增加一个网络API监听者,将数据推送到云端,而无需修改核心收集逻辑。

6. 避坑指南与最佳实践

在实际项目中应用监听者模式,我踩过不少坑,也总结出一些让代码更健壮、更高效的经验。

6.1 生命周期管理:谁拥有谁?

这是监听者模式中最容易出错的地方。核心问题是:观察者和主题,谁应该活得更久?

  • 最佳实践:主题拥有更长的生命周期,或者使用弱引用。通常,主题(如核心数据模型、事件总线)是应用程序中生命周期较长的对象。观察者(如UI控件、临时处理器)的生命周期可能较短。务必确保在观察者析构时,将其从所有主题的订阅列表中移除。上面的示例中,我们在观察者析构函数中调用Detach就是遵循这一原则。
  • 使用std::shared_ptrstd::weak_ptr:如前所述,这是现代C++管理跨对象生命周期的标准工具。主题持有weak_ptr,观察者自身由shared_ptr管理。这能有效避免悬空指针,但要注意避免循环引用(观察者持有主题的shared_ptr),此时观察者应持有主题的weak_ptr或原始指针/引用。
  • 考虑使用令牌(Token)模式Attach方法返回一个唯一的令牌(如整数ID或std::shared_ptr<void>),Detach时使用这个令牌。这比直接传递观察者指针更安全,尤其适用于基于std::function的信号槽系统。

6.2 通知的性能与顺序

  • 通知的性能:如果观察者数量很多(成千上万),Notify遍历列表可能成为性能瓶颈。优化方法包括:
    • 将耗时操作从Update中移出,Update只负责接收事件并放入队列,由另一个线程处理。
    • 使用更高效的数据结构,如std::vector(缓存友好),但要注意中间插入删除的成本。
    • 对于不需要立即处理的通知,可以考虑批量或异步通知。
  • 通知的顺序:监听者模式通常不保证通知的顺序。如果业务逻辑依赖通知顺序(比如A必须在B之前更新),你需要显式地管理这种依赖。可以在主题内部对观察者列表进行排序,或者让观察者通过优先级机制注册。
  • 在通知过程中修改列表:这是一个经典陷阱。如果在Notify的遍历过程中,某个观察者的Update方法里又调用了AttachDetach,会导致迭代器失效,程序崩溃。解决方案是前面提到的复制列表法,或者在修改列表时使用标志位延迟处理。

6.3 避免过度使用与设计变体

  • 不是银弹:监听者模式解耦了发布者和订阅者,但也可能使数据流变得难以追踪。一个事件被触发后,你很难一眼看出哪些模块会受到影响。调试时,需要跟踪整个通知链。在简单的一对一通信中,直接调用可能更清晰。
  • 内存泄漏:如果忘记取消订阅,主题会一直持有已销毁观察者的引用(原始指针场景),或者观察者因为被主题的shared_ptr持有而无法释放(智能指针场景)。务必在观察者析构时清理订阅关系。
  • 考虑“推”模型和“拉”模型
    • 推模型(Push):主题在通知时,将相关的数据作为参数传递给观察者。优点是观察者能立即拿到所需数据;缺点是主题需要知道观察者需要什么数据,可能传递了不必要的信息。
    • 拉模型(Pull):主题在通知时只传递自身的引用,观察者收到通知后,再主动从主题“拉取”需要的数据。优点是主题接口简单,观察者按需索取;缺点是观察者需要知道主题的查询接口,增加了耦合,且可能引发多次查询。
    • 在实践中,推模型更常用,尤其是结合std::anystd::variant或自定义事件数据类时,可以灵活地传递数据。

6.4 与现代C++框架集成

  • Qt的信号槽:Qt的信号槽机制是监听者模式的超集,它提供了线程安全的跨线程通信(Qt::AutoConnection)、异步调用(Qt::QueuedConnection)等高级特性。在Qt项目中,应优先使用其内置机制。
  • Boost.Signals2:这是一个功能强大的、线程安全的信号槽库,支持连接管理、槽分组、返回值聚合等。如果你需要一个功能齐全的第三方库,Boost.Signals2是绝佳选择。
  • C++标准库:对于简单的场景,结合std::functionstd::vectorstd::mutex,自己实现一个轻量级的信号系统是完全可行的,如上文所示。这避免了引入外部依赖。

监听者模式是构建松耦合、可扩展系统的强大工具。从经典的继承实现到现代的std::function信号槽,其思想一脉相承。理解其原理,认清其陷阱,并选择适合你项目场景的实现方式,就能让它在你的C++工具箱中发挥出最大的威力。记住,好的模式是仆人,而不是主人,永远根据实际需求来决定如何使用它。

http://www.jsqmd.com/news/1204685/

相关文章:

  • 具身智能四大技术路线:VLA、大小脑、世界模型与强化学习
  • Netty ByteToMessageDecoder 中 ByteBuf 的readerIndex 详解
  • ABAP Web Service集成SAML 2.0:从配置到用户映射的完整实战指南
  • Hermes便携IDE:U盘级AI Agent开发环境全平台部署指南
  • JWT双令牌认证与Redis黑名单:构建高可用分布式会话方案
  • Ventoy多系统启动U盘制作与优化全攻略
  • 【标题】2026年赤峰经济纠纷律师推荐 张洛溪7年民商法律实务经验 - 本地品牌推荐
  • C++多文件编程:从编译链接原理到项目工程化实践
  • C++性能优化实战:从硬件原理到代码调优的完整指南
  • 一文读懂 | 欧盟《网络弹性法案》(CRA)
  • agent学习Day5——Ruff、日志与结构化 LLM 输出
  • 冷柜温控器为什么要做远程监控和异常告警
  • Windows定时任务设置与schtasks命令详解
  • Python五子棋实战:从控制台到图形界面的完整开发指南
  • 人形机器人产品经理实战指南:从物理约束到场景落地
  • C++环境搭建全攻略:从零到一运行Hello World
  • 劳力士中国官方售后服务中心|地址与客户服务热线权威信息公示(2026年7月更新) - 劳力士服务中心
  • springboot新能源汽车充电桩预约与使用管理系统---附源码21146
  • 我见过最厉害的产品运营,都悄悄戒掉了“技巧”
  • C#动态调用C/C++ DLL:告别DLL地狱,实现优雅降级与跨平台部署
  • 先楫HPM5361EVK开发板与红外传感器集成开发指南
  • Windows 11 23H2架构解析与安装指南
  • Windows驱动开发必备:Windbg快捷键肌肉记忆训练指南
  • C++实现Raft共识算法:从状态机设计到异步工程实践
  • 2026 年现阶段凉山州靠谱的镀金镀银电子料回收制造企业深度解析,别再扔了!这批“废料”能帮你赚回多少钱? - 领域鉴赏官
  • VLA模型本质是动作片段搜索引擎,非通用机器人智能
  • C++ STL queue容器详解:从FIFO原理到多线程实战应用
  • Sherlock终极进化:从脚本到AI智能搜索平台的架构设计与实现
  • 激光雷达线数不是自动驾驶能力标尺:点云质量与系统协同才是关键
  • 2026年7月最新亨得利官方服务项目及价格查询|完整热线电话与网点地址权威信息公告 - 亨得利官方