裸指针在发布订阅中易导致未定义行为,因其不管理所有权,对象析构后调用会造成崩溃或数据损坏;应优先使用std::weak_ptr配合std::shared_ptr自动管理生命周期。

为什么裸指针在发布订阅里反而容易出错
直接用 std::shared_ptr 或 std::weak_ptr 管理订阅者,比裸指针安全得多。裸指针本身不表达所有权,一旦被指向的对象提前析构,publish() 时调用已释放内存的函数指针,就是未定义行为——轻则 crash,重则静默数据损坏。
常见错误现象包括:
- 订阅者对象析构后,发布端仍尝试通过裸指针调用其
on_event() - 多线程环境下,裸指针未加同步就访问,触发竞态(如一个线程正在 delete,另一个线程正在 call)
- 忘记手动清理指针数组,导致内存泄漏或重复释放
用 std::weak_ptr 配合 std::shared_ptr 自动管理生命周期
核心思路:让每个订阅者继承自 std::enable_shared_from_this,发布者只保存 std::weak_ptr;每次 publish 前用 lock() 尝试升级为 shared_ptr,失败即说明对象已销毁,自动跳过。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 订阅者类必须公有继承
std::enable_shared_from_this<Subscriber> - 注册时传入
shared_from_this(),发布者存为std::vector<std::weak_ptr<Subscriber>> - publish 循环中对每个
weak_ptr调用lock(),仅当返回非空shared_ptr才调用回调 - 避免在回调内部又去注册/注销自身——可能引发迭代器失效,应改用延迟删除(如标记 + 后续清理)
示例关键片段:
void Publisher::publish(const Event& e) {
for (auto& wptr : subscribers_) {
auto ptr = wptr.lock();
if (ptr) ptr->on_event(e);
}
}
性能敏感场景下如何减少 shared_ptr 开销
频繁 publish + 大量订阅者时,shared_ptr 的原子引用计数确实有可观开销(尤其在 ARM 或高并发下)。但别急着换裸指针——先确认是否真为瓶颈(用 perf / VTune 测一下 __atomic_fetch_add_8 占比)。
更稳妥的优化路径:
- 把订阅者按生命周期分组:长期存活的用
shared_ptr,短期任务用std::function+ 拷贝捕获值语义(避免指针) - 发布者内部用
std::vector<std::weak_ptr<T>>而非std::list,提升遍历 cache 局部性 - 若确定所有订阅者生命周期严格长于发布者,可用
std::observer_ptr<T>(C++17,无引用计数,但需人工保证不 dangling) - 极端情况可考虑 arena 分配 + 对象池,统一管理订阅者内存,配合 raw 指针 + epoch-based reclamation(但复杂度陡增)
多线程发布时最容易忽略的三个细节
很多人加了 mutex 就以为线程安全了,其实还差得远。
- 订阅/注销操作和 publish 必须共用同一把锁——不能 publish 用读锁、增删用写锁,因为
weak_ptr::lock()不是原子的,中间可能被析构 - 回调函数本身可能阻塞或抛异常,发布者不应假设它“瞬间完成”,建议在锁外调用回调(先拷贝出有效指针列表,再解锁,再逐个 call)
- 若允许从回调内注销自己,必须确保:① 当前正在遍历的 weak_ptr 不被 erase;② 使用
erase-remove惯用法或延迟队列,而非边遍历边删 vector
真正健壮的实现,往往在“高性能”和“完全自动生命周期管理”之间做取舍——选哪条路,取决于你的订阅者创建/销毁频率、是否跨线程、以及能否接受少量延迟注销。裸指针不是不行,只是把责任全推给了使用者,而多数业务代码并不值得为那几个纳秒去赌一把。



















