用shared_ptr易致循环引用:subject持observer的shared_ptr,observer又持subject的shared_ptr,双方引用计数无法归零,导致内存泄漏与析构失败;weak_ptr可破此局,observer仅用weak_ptr临时lock访问subject,不延长其生命周期。

为什么观察者模式里用 shared_ptr 容易导致循环引用死锁
观察者模式中,被观察者(Subject)通常持有一组观察者(Observer)的指针;而观察者又常常持有对 Subject 的引用以便主动获取状态或调用更新接口。如果双方都用 shared_ptr 持有对方,就会形成循环引用:Subject 的引用计数依赖 Observer,Observer 的引用计数又依赖 Subject,导致两者永远无法析构。
典型现象是程序退出时内存没释放、析构函数不执行,或者调试时发现对象生命周期远超预期。尤其在长期运行的服务中,这类泄漏会逐渐累积。
- Subject 保存
std::vector<:shared_ptr>></:shared_ptr> - Observer 内部保存
std::shared_ptr<subject></subject>(比如用于调用Subject::getState()) - 一旦注册后未手动解除,两者相互延长生命周期,
weak_ptr是唯一安全解法
如何用 weak_ptr 在 Observer 中安全访问 Subject
Observer 不该拥有 Subject,只应在需要时临时访问。用 std::weak_ptr<subject></subject> 替代 shared_ptr,并在每次使用前调用 lock() 获取临时有效的 shared_ptr —— 如果返回空,说明 Subject 已销毁,直接跳过本次通知。
关键不是“避免崩溃”,而是让 Observer 能优雅退场:即使 Subject 先销毁,Observer 仍可存在(比如等待异步任务结束),只是后续调用失效。
- Observer 构造时接收
std::weak_ptr<subject></subject>,而非shared_ptr - 在
update()中写:auto subj = subject_.lock(); if (!subj) return; - 不要把
lock()结果存为成员变量——那又变相延长了生命周期 -
weak_ptr本身不增加引用计数,也不参与析构顺序竞争
Subject 注册/注销时怎么避免迭代器失效和重复删除
Subject 管理观察者列表时,常用 std::vector<:shared_ptr>></:shared_ptr>。但注销(unregister)若用 erase(remove_if(...)) 配合裸指针比较,容易因对象已析构而访问非法内存;若用 shared_ptr 比较,则需确保 Observer 实例唯一且未被拷贝。
更稳妥的做法是:注册时返回一个 std::size_t token(下标),注销时按索引删;或用 std::list + erase 迭代器,配合 std::shared_ptr::owner_before 做唯一性判断。
- 推荐注册返回
std::shared_ptr<void></void>作为 token(利用其控制块地址唯一性) - 内部用
std::set<:shared_ptr>, std::owner_less<...>></...></:shared_ptr>存 token,避免重复插入 - 注销时直接 erase 对应 token,无需遍历比较 Observer 对象内容
- 注意:不能用原始指针做 key,因为 Observer 可能已析构,地址复用会导致误删
一个最小可运行的 C++20 版本示例(含完整生命周期验证)
下面代码去掉异常处理和线程安全,聚焦核心逻辑。重点看 Observer 如何用 weak_ptr、Subject 如何用 owner_less 管理 token:
#include <memory>
#include <set>
#include <vector>
#include <iostream>
struct Subject;
struct Observer {
explicit Observer(std::weak_ptr<subject> s) : subject_(s) {}
void update() {
auto subj = subject_.lock();
if (!subj) return;
std::cout value() subject_;
};
struct Subject {
using Token = std::shared_ptr<void>;
std::set<token std::owner_less>> observers_;
int data_ = 42;
Token register_observer(std::shared_ptr<observer> obs) {
auto token = std::make_shared<int>(0);
observers_.insert(token);
return token;
}
void unregister_observer(const Token& t) {
observers_.erase(t);
}
void notify() {
// 复制一份智能指针,避免遍历时容器被修改
auto copies = std::vector<:shared_ptr>>();
for (auto& t : observers_) {
// 这里实际应关联 observer 实例,简化起见略去映射逻辑
}
// 真实项目中建议用 map<token shared_ptr>>
}
int value() const { return data_; }
};
</token></:shared_ptr></int></observer></token></void></subject></iostream></vector></set></memory>
真正麻烦的不是写出来,而是验证它是否真的不会循环引用——用 AddressSanitizer 编译,注册后立即释放 Subject,再触发一次 notify,看 Observer 是否静默跳过。这点很容易被忽略。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











