字节面试官:问你C++观察者模式,你答了7点他说不够深

二面技术官问了你一道看似简单的问题:“用C++实现一个观察者模式,说说关键点。”

你噼里啪啦说了一通:接口设计、注册注销、通知机制……自我感觉良好。结果他皱了皱眉说:“这些是基础,我想听的是C++特有的实现难点。”

那一刻你才意识到,观察者模式用Java、Python实现和用C++实现,根本不是一回事——C++没有GC,指针满天飞,稍不注意就是野指针崩溃,所以面试官想听的是你对C++内存管理、线程安全、异常处理这些底层问题的理解,而不是设计模式本身的概念。今天我总结出了这7个C++观察者模式的实现关键点,今天分享给你。


一、接口设计:虚函数还是std::function?

传统教科书告诉我们观察者模式要定义抽象基类:

classIObserver{public:virtual~IObserver()=default;virtualvoidonNotify(constEvent&event)=0;};

这种设计没毛病,但有个实际问题:每个想监听事件的类都得继承这个接口,如果一个类想同时监听多种事件怎么办?多重继承会让接口迅速膨胀,代码也变得难以维护。

现代C++的做法是用std::function替代继承:

classSubject{public:usingCallback=std::function<void(constEvent&)>;intsubscribe(Callback cb){intid=nextId_++;callbacks_[id]=std::move(cb);returnid;}voidunsubscribe(intid){callbacks_.erase(id);}voidnotify(constEvent&event){for(auto&[id,cb]:callbacks_){cb(event);}}private:intnextId_=0;std::unordered_map<int,Callback>callbacks_;};

std::function的好处是灵活:可以传成员函数、lambda、甚至另一个函数对象,观察者不需要继承任何东西。代价是有一定的性能开销(类型擦除加上可能的堆分配),不过对大多数业务场景来说这点开销完全可以忽略。

关键点:接口设计的选择取决于场景——如果观察者类型固定且追求极致性能就用虚函数,如果需要灵活性就用std::function


二、注册/注销机制:返回值设计很重要

很多人实现观察者模式的时候注册函数写成这样:

voidsubscribe(IObserver*observer);voidunsubscribe(IObserver*observer);

这样设计有个隐患:如果同一个observer注册了两次,unsubscribe的时候是把两个都删掉还是只删一个?行为不明确,调用方很容易踩坑。

更好的做法是返回一个唯一ID或者token,我更推荐用RAII封装成Subscription类:

classSubscription{public:Subscription()=default;Subscription(Subject*subject,intid):subject_(subject),id_(id){}~Subscription(){if(subject_){subject_->unsubscribe(id_);}}// 禁止拷贝,允许移动Subscription(constSubscription&)=delete;Subscription&operator=(constSubscription&)=delete;Subscription(Subscription&&other)noexcept;Subscription&operator=(Subscription&&other)noexcept;private:Subject*subject_=nullptr;intid_=-1;};

这个设计的精妙之处在于用RAII管理订阅生命周期:对象销毁时自动取消订阅,调用者只需要保存这个Subscription对象就行,不用手动调unsubscribe,也不会因为忘记取消订阅导致野指针崩溃。

关键点:用RAII封装订阅关系让编译器帮你管理生命周期,比依赖程序员记得手动调用靠谱得多。


三、生命周期管理:这是C++的命门

Java程序员可能不理解为什么生命周期管理这么重要,因为他们有GC——只要有引用对象就不会被回收。但C++没有这个待遇,你必须自己管理每个对象的生死。

考虑这种情况:

classObserver:publicIObserver{voidonNotify(constEvent&e)override{// 处理事件}};voidfoo(){Observer obs;subject.subscribe(&obs);}// obs析构了,但subject还持有它的指针!subject.notify(event);// 崩溃:访问已销毁的对象

这就是典型的悬垂指针问题,生产环境中这种bug排查起来极其痛苦,因为崩溃点和问题根源往往相隔甚远。解决方案有两种主流做法:

方案一:weak_ptr + shared_ptr

classSubject{public:voidsubscribe(std::weak_ptr<IObserver>observer){observers_.push_back(observer);}voidnotify(constEvent&event){observers_.erase(std::remove_if(observers_.begin(),observers_.end(),[&event](auto&weak){if(autostrong=weak.lock()){strong->onNotify(event);returnfalse;}returntrue;// 已失效,移除}),observers_.end());}private:std::vector<std::weak_ptr<IObserver>>observers_;};

这种方式通过weak_ptr检测观察者是否还活着:如果已经销毁就自动从列表中移除,非常安全。缺点是要求观察者必须用shared_ptr管理,有时候这个约束太强了——特别是当观察者是栈上对象或者由其他生命周期管理机制控制的时候。

方案二:RAII Subscription(前面讲过的那个)

这是我更推荐的方式,因为它不强制观察者的内存管理方式,只要保证Subscription的生命周期不超过观察者就行,灵活性更高。

关键点:C++的观察者模式必须显式处理生命周期问题,要么用智能指针约束,要么用RAII自动管理——千万不能寄希望于程序员记得手动取消订阅。


四、通知过程中的增删问题:迭代器失效陷阱

想象一个场景:Subject正在遍历观察者列表发通知,某个观察者收到通知后决定取消自己的订阅,或者注册一个新的观察者。

voidSubject::notify(constEvent&event){for(auto&observer:observers_){// 正在遍历observer->onNotify(event);// 回调里可能调用unsubscribe!}}

如果onNotify里面调用了unsubscribe,那observers_容器就被修改了,当前的迭代器立刻失效,程序直接崩溃。这个问题在复杂系统中特别常见,因为回调函数的行为往往不可预测。

解决办法有两种:

方案一:遍历副本

voidSubject::notify(constEvent&event){autocopy=observers_;// 先复制一份for(auto&observer:copy){observer->onNotify(event);}}

简单粗暴:先复制一份列表再遍历,原列表怎么改都不影响当前遍历。缺点是如果观察者列表很大,每次notify都要复制一遍,开销不小。

方案二:延迟删除

classSubject{public:voidnotify(constEvent&event){notifying_=true;for(auto&observer:observers_){if(!observer.removed){observer.callback(event);}}notifying_=false;// 遍历结束后再真正执行删除observers_.erase(std::remove_if(observers_.begin(),observers_.end(),[](auto&o){returno.removed;}),observers_.end());}voidunsubscribe(intid){autoit=findById(id);if(notifying_){it->removed=true;// 只标记,不删除}else{observers_.erase(it);// 立即删除}}private:boolnotifying_=false;// ...};

延迟删除稍微复杂一点,但避免了复制开销,适合观察者列表较大或者notify调用非常频繁的场景。

关键点:通知过程中的增删操作会导致迭代器失效,必须特殊处理。列表小就用副本遍历简单可靠,列表大就用延迟删除节省开销。


五、线程安全:锁的粒度是门艺术

如果Subject和Observer可能在不同线程中操作,问题就更复杂了。最直接的做法是加锁:

classSubject{public:voidsubscribe(Callback cb){std::lock_guard<std::mutex>lock(mutex_);callbacks_.push_back(std::move(cb));}voidnotify(constEvent&event){std::lock_guard<std::mutex>lock(mutex_);for(auto&cb:callbacks_){cb(event);// 问题:持锁调用回调!}}private:std::mutex mutex_;std::vector<Callback>callbacks_;};

看起来线程安全了对吧?其实藏着死锁风险:如果回调函数里又调用了subscribe或unsubscribe,就会发生递归加锁,普通的std::mutex直接死锁——整个系统卡死。

改进方案:缩小锁的粒度

voidSubject::notify(constEvent&event){std::vector<Callback>snapshot;{std::lock_guard<std::mutex>lock(mutex_);snapshot=callbacks_;// 持锁复制}// 释放锁之后再调用回调for(auto&cb:snapshot){cb(event);}}

先在锁保护下复制一份回调列表,然后释放锁再遍历调用,这样回调函数里可以自由地subscribe和unsubscribe而不会死锁。如果追求更高性能可以考虑无锁数据结构或者用读写锁(std::shared_mutex)来区分读多写少的场景,但大多数情况下上面的方案已经够用了。

关键点:多线程场景下绝对不要在持锁状态调用用户回调函数,否则极易死锁。先复制再调用是最稳妥的做法。


六、异常安全:通知链条别断掉

如果某个观察者的回调函数抛出异常会怎样?

voidSubject::notify(constEvent&event){for(auto&cb:callbacks_){cb(event);// 这里抛异常的话,后面的观察者就收不到通知了}}

默认情况下异常会中断循环,导致后续的观察者收不到通知。这在很多场景下是不可接受的——一个观察者的bug不应该影响整个系统的通知机制。

解决方案:捕获并记录

voidSubject::notify(constEvent&event){for(auto&cb:callbacks_){try{cb(event);}catch(conststd::exception&e){// 记录日志,继续通知下一个std::cerr<<"Observer threw: "<<e.what()<<std::endl;}catch(...){std::cerr<<"Observer threw unknown exception"<<std::endl;}}}

这样即使某个观察者抛异常也不会影响其他观察者收到通知。至于异常怎么处理(是记日志、重试、还是移除该观察者),取决于你的业务需求和异常严重程度。

关键点:在notify循环中加try-catch保护,保证一个观察者的异常不会影响其他观察者的正常通知。


七、性能优化:别让观察者模式成为瓶颈

说了这么多安全性问题,最后聊聊性能,毕竟有些场景下观察者模式的调用频率非常高。

1. 容器选择

不同的容器特性差异很大:

  • std::vector:遍历最快,适合频繁notify但较少subscribe/unsubscribe的场景
  • std::list:插入删除是O(1),但遍历有额外的指针追踪开销
  • std::unordered_map:用ID做key删除是O(1),遍历稍慢但支持随机删除

大多数情况下用vector配合ID查找(O(n)删除)就够用了,除非你的观察者列表有成百上千个。

2. 避免不必要的复制

如果Event对象很大,用const Event&或者std::shared_ptr<const Event>传递,避免每次notify都复制一遍Event对象,这个开销在高频调用场景下会非常可观。

3. 小对象优化

如果用std::function,尽量让你的回调是小对象(能放进small buffer optimization的缓冲区),避免堆分配。Lambda捕获太多变量就会触发堆分配从而影响性能,实测差距可达10倍以上。

关键点:先保证正确性再考虑性能。大多数场景下观察者模式不会成为瓶颈,但如果真的遇到性能问题,从容器选择和复制开销入手优化效果最明显。


总结

如果让你重新回答,你会这样子说了:

C++实现观察者模式比其他语言多了不少坑,核心难点在于没有GC的情况下如何安全地管理观察者的生命周期和处理各种边界情况。这7个关键点可以分成三类:

设计层面:

  1. 接口设计:虚函数 vs std::function,根据灵活性需求选择
  2. 注册机制:返回RAII封装的Subscription,自动管理订阅生命周期

安全层面:
3. 生命周期:用weak_ptr或RAII避免悬垂指针
4. 迭代器失效:通知时增删观察者要用副本或延迟删除
5. 线程安全:不要持锁调用回调,避免死锁
6. 异常安全:try-catch保护,一个观察者异常不能影响其他

性能层面:
7. 容器选择、避免复制、小对象优化

这7点答全了,面试官应该挑不出毛病来了吧?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1196691.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分流抢票软件bypass,Bypass-分流抢票:让你秒杀抢票,稳定捡漏的神器!

马上要过年了。你的火车票好买吗&#xff1f;希望大家都能买到心仪的火车票&#xff0c;开开心心&#xff0c;安安全全回家过年。bypass是一款抢票服务类软件&#xff0c;系统会实时定位用户位置&#xff0c;快速搜索附近列车信息&#xff0c;在线改期可在线进行&#xff0c;延…

springboot_ssm808图书借阅挂失崔还系统功能全--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着信息化技术的快速发展&#xff0c;图书馆管理系统逐渐从传统的手工管理模式向数字化、智能化方向转变。基于Sprin…

K8s修改Pod的Command/Args参数报错?这篇实操指南帮你搞定

K8s修改Pod的Command/Args参数报错&#xff1f;这篇实操指南帮你搞定在Kubernetes日常运维中&#xff0c;修改Pod配置是很常见的操作&#xff0c;但不少同学会遇到类似“Pod updates may not change fields other than...”的报错&#xff0c;尤其是修改容器启动命令&#xff0…

K8s Nginx Pod 出现 CrashLoopBackOff?从配置排查到彻底解决

在Kubernetes日常运维中&#xff0c;Pod处于CrashLoopBackOff状态是高频问题之一。近期在部署Nginx Pod时&#xff0c;就遇到了这类故障&#xff0c;同时Redis Pod正常运行&#xff0c;说明集群环境无异常&#xff0c;问题聚焦在Nginx Pod自身配置。本文结合实操过程&#xff0…

Ubuntu系统移植

一、移植准备工作 1、安装库 sudo apt-get install u-boot-tools sudo apt-get install libyaml-dev sudo apt-get install libssl-dev sudo apt-get install flex sudo apt-get install bison sudo apt-get install libncurses-dev sudo apt-get install gparted sudo ap…

【奖励到账】CSDN AI 社区镜像创作激励活动第三批奖励正式发放!

家人们注意啦&#xff01;CSDN AI 社区镜像创作激励活动第二批现金奖励已正式发放&#xff0c;CSDN再次新增开发者 “技术变现” 赛道&#xff0c;还没参与的你&#xff0c;速来 get 这份躺赢攻略&#xff0c;下一批奖励名单说不定就有你&#xff01; 1月21日&#xff1a;第三…

[特殊字符] 最新版 | Windows10 Win11系统终极优化神器RyTuneX完全安装配置指南 [特殊字符]

RyTuneX是一款基于‌WinUI 3框架开发‌的Windows系统优化工具&#xff0c;专门为‌Windows 10/11‌设计&#xff0c;具备一键清理、性能提升、隐私保护等强大功能。本文将手把手教你如何完整安装、配置并发挥其最大效能&#xff0c;助力系统焕然一新&#xff01; 【W10 Win11系…

springboot_ssm809基于SSM架构的网上书城系统图书销售--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着互联网技术的快速发展&#xff0c;电子商务已成为现代商业活动的重要组成部分。图书销售作为传统行业之一&#x…

全面优化你的Windows,RyTuneX系统全能优化神器

一、简介 RyTuneX​ 是一款基于WinUI 3框架和**.NET 8开发的现代化Windows系统优化工具&#xff0c;专为Windows 10/11**设计。这款windows优化工具提供一站式系统优化解决方案&#xff0c;让用户无需专业知识即可轻松提升系统性能、清理冗余和保护隐私。 ​ 目前是1.6.0 &…

【课程设计/毕业设计】基于SpringBoot的宝贝回家走失儿童报备系统基于springboot的走失儿童认领与登记系统【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

springboot_ssm810基于SSM的校园音乐平台--论文

目录 具体实现截图摘要 系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01; 具体实现截图 摘要 随着互联网技术的快速发展&#xff0c;校园音乐文化逐渐成为学生生活的重要组成部分。传统的校园音乐活动受限于时…

springboot_ssm811基于web的特殊药品商城管理系统--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着互联网技术的快速发展&#xff0c;电子商务系统在各行业的应用日益广泛。特殊药品作为医疗领域的重要组成部分&am…

Flux2 Klein 闪电急速出图 WebUI整合包体验版下载及使用教程【上篇】(模型与性能解析)

Flux2 Klein 闪电急速出图 WebUI整合包体验版下载及使用教程【上篇】&#xff08;模型与性能解析&#xff09; 关键词&#xff08;SEO&#xff09;&#xff1a; Flux2、Flux Klein、AI绘画整合包、WebUI整合包、FP8模型、AI出图速度 前言&#xff1a;第一次用 Flux2&#xff0…

淘宝大秒系统设计详解:从技术挑战到架构演进

一、秒杀系统核心挑战与技术背景1.1 秒杀场景的技术特征秒杀作为电商平台中最极致的营销模式&#xff0c;其技术挑战远超普通电商交易场景。淘宝大秒系统面对的典型场景包括&#xff1a;瞬时流量特征&#xff1a;访问量暴增&#xff1a;日常QPS可能从几千瞬间飙升到数百万级别读…

提示工程架构师实战分享:教育领域Agentic AI的5大技术挑战及解决案例

提示工程架构师实战分享&#xff1a;教育领域Agentic AI的5大技术挑战及解决案例 引言&#xff1a;当Agentic AI走进教室——机遇与痛点 清晨的教室&#xff0c;张老师看着讲台上堆成小山的作业&#xff0c;揉了揉太阳穴&#xff1a;35个学生&#xff0c;每个人的学习进度、薄…

科研数据AI分析工具,AI应用架构师的数据分析新策略

科研数据AI分析工具&#xff1a;AI应用架构师的数据分析新策略 一、引言 (Introduction) 钩子 (The Hook) “全球科研数据正以每两年翻一番的速度爆炸式增长&#xff0c;2025年预计达到175ZB——这相当于2.5亿个 Libraries of Congress 的数据量。” 当一位生物学家面对TB级的基…

Flux2 Klein WebUI整合包下载及实操教程【下篇】(分辨率 / 显存 / 批量出图)

Flux2 Klein WebUI整合包下载及实操教程【下篇】&#xff08;分辨率 / 显存 / 批量出图&#xff09; 关键词&#xff08;SEO&#xff09;&#xff1a; Flux2 使用教程、WebUI 参数设置、AI绘画新手教程、FP8 模型使用方法 整合包下载地址&#xff08;体验版&#xff09; &…

springboot_ssm812基于推荐算法的图书购物网站--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着互联网技术的快速发展&#xff0c;电子商务平台在图书销售领域的应用日益广泛。传统的图书购物网站通常依赖简单的…

程序员入门教程【非常详细】从零基础入门到精通,看完这一篇就够了 !

零基础编程入门先学什么&#xff1f;编程语言有几百种&#xff0c;我们应该怎么选择。想学习编程&#xff0c;加入互联网行业&#xff0c;哪一个更有前途&#xff1f;在小白学习编程会有各种各样的问题&#xff0c;今天我就来为你解答。 一、怎么选择编程语言 编程语言有很多种…

CF621E-Wet Shark and Blocks

CF621E-Wet Shark and Blocks 题目大意 你现在一共有 b b b 堆一模一样的数字&#xff0c;每堆数字中有 n n n 个 1 − 9 1-9 1−9 的一位数。你现在可以从每一堆里恰好选一个数&#xff0c;将这些数从左到右拼成一个大数。将这个拼成的大数对 x x x 取模&#xff0c;问你…