大疆 | C++开发工程师 二面 面经

1. 介绍一个你主导的最复杂的项目,架构是怎么设计的,遇到的最难的技术问题是什么,怎么解决的?

答:这道题是二面的开场,考察系统思维和技术深度,回答要有结构。

建议按三段走:背景和目标(项目是什么、规模、你的角色)→ 架构设计思路(为什么这么分层、核心模块怎么划分、关键技术选型理由)→ 最难的问题(要具体,不能说"遇到了很多困难",要说清楚是什么问题、为什么难、排查过程、最终方案)。

比如车载物联项目,可以说:系统需要同时管理30+种异构设备,设备协议差异大,所以设计了HAL抽象层屏蔽硬件差异,上层统一用设备模型操作;最难的问题是多设备并发上报状态时MQTT消息乱序,排查发现是QoS1下broker重传和客户端处理顺序不一致,最终在应用层加了序列号和滑动窗口做重排。

面试官会顺着你说的细节追问,所以只说你真正做过的,每个数字背后要有支撑。

2. 你在项目中设计过通信协议吗?如何保证协议的可靠性、扩展性和向后兼容性?

答:协议设计是嵌入式和IoT项目的核心能力,考察点很多。

可靠性层面:需要考虑数据完整性校验(CRC16/CRC32,比简单checksum抗突发错误能力强)、序列号机制(检测丢包和乱序)、ACK/重传机制(超时重传,指数退避避免风暴)、心跳保活(检测链路静默断开)。

扩展性层面:协议头要预留版本号字段,消息体用TLV(Type-Length-Value)格式,新增字段不影响旧版本解析,接收方遇到未知Type直接跳过。避免用固定偏移解析,否则加字段就是破坏性变更。

向后兼容性:版本协商在握手阶段完成,双方取min(本端版本, 对端版本)作为通信版本;废弃字段保留但标记deprecated,不直接删除;消息ID不复用,只新增。

实际项目里还要考虑大小端问题,跨平台传输统一用网络字节序(大端),接收端用ntohl/ntohs转换。

3. 你提到做过FOTA升级,请详细说明一个可靠的OTA升级方案应该包含哪些环节,如何保证升级失败后设备不变砖?

答:可靠OTA是嵌入式产品的生命线,方案要覆盖完整的失败路径。

升级包准备阶段:服务端对升级包做数字签名(RSA或ECDSA),设备端验签,防止恶意固件注入。包内包含版本号、目标硬件型号、CRC校验值,设备下载完成后先校验再安装。支持差分包(bsdiff/xdelta)减少传输量,在带宽受限的cat1/NB-IoT场景尤其重要。

防变砖的核心是双分区(A/B分区)方案:Flash划分为两个系统分区,当前运行A,新固件写入B,写入完成校验通过后修改bootloader的启动标志指向B,重启。Bootloader启动时检查B的完整性,如果B启动失败(启动计数超过阈值),自动回滚到A。整个过程A分区始终保持可用状态。

如果Flash空间不够做双分区,可以用Recovery分区方案:主分区+一个只读的最小Recovery系统,主分区升级失败时进入Recovery,从服务器重新拉取固件。

升级过程中断电处理:写Flash前先写一个"升级进行中"标志到独立的状态区,升级完成后清除。Bootloader启动时检查这个标志,如果存在说明上次升级未完成,直接回滚。

4. 在你的项目中,如何设计一个支持多硬件平台的HAL层?抽象的边界怎么划定?

答:HAL设计的核心矛盾是:抽象太薄,上层还是要关心硬件细节;抽象太厚,性能损失大,而且很难覆盖所有硬件特性。

边界划定原则:HAL只暴露功能语义,不暴露硬件寄存器细节。比如串口HAL提供open/read/write/close/set_baudrate,不暴露具体的UART控制寄存器地址。但硬件特有的能力(比如某芯片支持DMA传输)可以通过ioctl风格的扩展接口暴露,上层按需使用,不强制依赖。

接口设计上,用纯虚基类或函数指针表(C风格)定义抽象接口,每个平台提供一个实现。C++项目用虚函数更自然,但在极度性能敏感的路径上可以用模板静态多态(CRTP)消除虚函数开销。

版本管理:HAL接口一旦发布就不能随意修改,新增接口用新的版本号标记,旧实现提供默认空实现,保证新HAL接口在旧平台上编译不报错。

测试:HAL层要有对应的Mock实现,上层业务逻辑的单元测试不依赖真实硬件,用Mock替代,这样CI流水线在没有硬件的环境下也能跑。

5. 请描述一次你处理过的最复杂的多线程bug,是如何定位和解决的?

答:这道题考察多线程调试经验,要有具体的排查过程。

典型的高难度多线程bug是偶发性数据损坏,没有明显崩溃,只是偶尔出现逻辑错误。排查思路:

第一步,确认是多线程问题还是单线程逻辑bug。在测试环境把线程数减到1,如果问题消失,基本确认是竞态。

第二步,用ThreadSanitizer(TSan)编译运行,它能检测数据竞争,会报告哪两个线程在没有同步的情况下访问了同一块内存,并给出调用栈。这是最高效的工具。

第三步,如果TSan没有直接命中,检查所有共享数据的访问路径,重点看:锁的粒度是否足够(是否有先读后写的非原子操作)、条件变量是否有虚假唤醒保护(while循环而不是if)、是否有double-checked locking但没有用atomic。

一个真实场景:状态机的状态变量用int存储,多个线程读写,在x86上因为int读写本身是原子的所以大部分时候没问题,但在ARM上32位非对齐访问可能被拆成两次操作,导致读到中间状态。修复方案是改用std::atomic

6. C++的模板特化和偏特化在工程中有哪些实际应用?你用过哪些基于模板的设计技巧?

答:模板特化在工程里最常见的用途是类型萃取和策略分发。

全特化的典型场景:为特定类型提供优化实现,比如序列化模板对std::string特化,直接写长度+内容,而不是逐字节处理。

偏特化的典型场景:std::vector

CRTP(奇异递归模板模式)是最常用的设计技巧,用于静态多态。基类模板接受派生类作为模板参数,在基类里调用派生类的方法,编译期绑定,没有虚函数开销。飞控这种高频控制循环里,用CRTP替代虚函数可以节省可观的开销。

类型列表(TypeList)和编译期计算:用模板递归实现编译期的类型操作,比如根据类型自动生成消息ID、自动注册序列化函数,减少手写注册代码的遗漏风险。

Policy-based design:把算法策略作为模板参数传入,比如内存分配策略、日志策略,编译期组合,零运行时开销,比运行时策略模式更高效。

7. 你在项目中用过哪些性能优化手段?请举一个具体的优化案例,说明优化前后的对比和你的分析方法。

答:性能优化要先测量再优化,不能凭感觉。

分析方法:首先用perf stat看整体CPU利用率和cache miss率,再用perf record + perf report做采样分析,找到热点函数。也可以用gprof或Valgrind的callgrind工具。确定热点后,分析是CPU bound还是memory bound(cache miss多说明是内存访问模式问题)。

一个具体案例:设备状态上报模块,每秒处理1000条状态消息,CPU占

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++八股文全集 文章被收录于专栏

本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务