observer接口必须声明virtual析构函数,否则通过基类指针删除派生类对象将导致析构不完整;通知时不可边遍历边erase,应采用两阶段清理或weak_ptr+lock机制;多线程下需用shared_mutex保护容器并避免锁内调用回调。

Observer 接口设计必须用虚析构函数
不加 virtual ~Observer() = default; 会导致派生类对象通过基类指针删除时析构不完整,内存泄漏或未定义行为。尤其在 Subject 持有 std::vector<:unique_ptr>></:unique_ptr> 时,这点非常关键。
常见错误现象:观察者对象的析构函数没被调用,资源(如文件句柄、线程)未释放;调试时发现 Observer 的析构日志完全没输出。
实操建议:
- 所有抽象基类的析构函数都声明为
virtual,哪怕函数体为空 - 避免裸指针管理观察者生命周期;优先用
std::unique_ptr或std::shared_ptr,但要注意循环引用风险 - 若用
std::shared_ptr,Subject内部存std::weak_ptr<observer></observer>,防止观察者持有Subject引用时形成强循环
notify() 中遍历容器时不能直接 erase 迭代器
在通知过程中移除观察者(比如某个观察者处理失败后主动退订),若边遍历边调用 vector::erase() 或 list::erase(),会令迭代器失效,引发崩溃或跳过下一个元素。
使用场景:网络模块中某个 TcpConnectionObserver 收到断连通知后自行注销;UI 控件销毁前解绑数据观察者。
实操建议:
- 用两阶段方式:先收集待移除的观察者标识(如指针或 ID),通知完成后再批量清理
- 改用
std::list存储观察者,并用erase()返回的迭代器继续遍历(C++11 起支持) - 更稳妥的做法是把
notify()设计为只读操作,解绑必须由外部同步调用detach(),并在Subject内部用std::weak_ptr+lock()检查有效性
std::function + std::bind 实现无接口的轻量订阅
不想写一堆继承 Observer 的类?可以用回调函数替代传统虚函数接口,降低耦合和模板膨胀。
性能影响:std::function 有小对象优化,但捕获大对象或频繁调用时可能触发堆分配;比虚函数调用略慢,但开发效率提升明显。
实操建议:
- 定义
using Callback = std::function<void event>;</void>,Subject内用std::vector<callback></callback> - 绑定成员函数时用
std::bind(&MyHandler::onUpdate, &handler, std::placeholders::_1),注意对象生命周期必须长于Subject - 推荐搭配
std::shared_ptr和弱回调:用std::weak_ptr包裹 handler,回调内部先lock(),失败则自动跳过
多线程下 attach/detach/notify 必须加锁
多个线程同时调用 attach() 和 notify(),或一个线程在遍历观察者列表时另一个线程正在 detach(),大概率导致迭代器失效或访问已释放内存。
容易踩的坑:只对容器修改加锁,但忘了 notify() 内部调用的回调函数也可能修改共享状态——锁粒度不够,或锁位置不对。
实操建议:
- 用
std::shared_mutex(C++17):读多写少场景下,notify()用共享锁,attach()/detach()用独占锁 - 避免在锁内调用用户回调;把要通知的观察者快照复制到局部容器,再解锁后逐个调用
- 若回调本身也涉及复杂同步(如更新 UI),考虑用消息队列或信号槽机制解耦,而不是在
notify()里硬等
真正麻烦的不是模式结构,而是生命周期管理与线程安全的交织——观察者挂了但 Subject 还存着它的指针,或者两个线程一删一调,core 就来了。这些地方没有银弹,只能根据具体场景选锁、选智能指针、选回调策略。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











