C++ 简历面试题与详细答案
初版 2026-09-05,2026-09-06 按 v4 简历更新。对应 ../resume/resume_cpp_v4.md,配合 ../resume/resume_cpp_v3_review_2026-09-05.md 与事实底稿 ../docs/profile.md 使用。
v4 相对 v3 的主要变化,已在本文对应题目中反映:两段任职合并为德科信息一段(2024.03 - 至今)并新增开发组长职级;恢复「1k+ 行」「20KB」两个量化数据;补入回调先缓存后投递、自定义 deleter、端口转发、心跳保活、shim 适配层等技术点;技能区新增 socket / TCP、GDB 与 AI 辅助开发。
使用说明
这份手册给出完整的技术解释和答题思路,不把未知代码实现编造成你的经历。涉及个人项目的开场,只采用 v4 已写的信息;后续的设计、测试与改进方案不代表你已经实施。实际函数名、线程数、错误日志、测试结果要以你的代码为准。
没有做过时可以答:“这部分没有做过完整落地。我实际负责的是……;如果扩展到这个要求,我会……”。不要把“不知道”改成“别的同事做的”,除非确实如此。
P0:先准备,与你的简历直接相关。P1:通用补充或收到相关面试后准备。每题先用 30—60 秒讲主答案,面试官追问再展开。技术问题通常有条件和取舍,不存在背一句就适用于所有场景的“唯一答案”。
一、介绍与经历边界
01|P0:用 90 秒介绍自己
参考回答
我有约四年软件开发经验,2024 年 3 月起在德科信息做 C++ 系统开发,目前是开发组长。之前两年做三维可视化方向,界面交互和底层组件、运行时适配都接触过。
这两年半的工作集中在三块。一是设备侧 OTA 升级,负责 flashd 的刷写与升级模块,做差分和连续升级的校验链路,以及补丁分区与镜像方案。二是从零设计了一个多协议终端客户端,负责 C++ 协议层、与界面的异步交互,以及多会话的并发和资源管理。三是把 PyTorch 的 CPU 运行时移植到 aarch64-musl 设备,处理交叉编译、libc 差异和 Python 扩展加载。
我希望继续做 Linux C++ 应用或系统组件开发,能拿出来的主要是并发与生命周期管理、跨平台构建,以及运行时问题定位。
为什么这样组织:先给年限和职级定位,再按「设备侧 → 应用架构 → 大型工程构建」给三块,最后落到求职方向。不逐行念技术栈,面试官手里有简历。
必须按岗位换顺序:三块的讲述顺序要让最相关的排最前。投嵌入式或设备类岗位先讲 OTA;投应用、客户端类先讲终端客户端;投构建、基础设施类先讲 PyTorch 移植。不要每场都用同一个顺序。
边界:
- 「四年开发」不能说成「四年 C++」。C++ 方向是 2024.03 起,约两年半,被追问要如实区分。岗位要求「3 年以上 C++」时,可以争取但要说清实际年限。
- 「系统开发」指用户态系统组件,不要让人理解成内核或驱动开发。
- 简历里 2024.03 至今合并为德科信息一段,期间派驻项目有变化(升级子系统 → PC 工具链)。被问到项目归属如实说明,不必主动展开。
- 被问「为什么从三维可视化转系统开发」,给真实原因,再用这两年半的连续项目说明方向稳定,不要编造公司安排。
- 目前是开发组长,被追问带人情况见下一题。
02|P0:你的个人贡献是什么?20+ 工具如何计算?
回答方法:把工作分为“我独立实现的模块”“共同参与的部分”“调用或复用的现成库”。例如协议库提供 SSH 能力,而你的价值可能在会话抽象、线程交互、生命周期和产品集成,不能说自己实现了 SSH 协议栈。
20+ 工具要按真实清单解释:一个工具的不同构建版本是否只计一个,哪些只是编译跑通,哪些改了 API/构建/界面,哪些由你主要负责。若数字是团队累计,应明确“参与团队……”,并挑自己最熟的一款展开。
追问:最难的 bug? 按“现象 → 复现条件 → 排除过什么 → 根因 → 修改 → 回归”讲一件真实事。只说“ABI 不兼容,所以修 ABI”不够;至少能落到某类符号、依赖来源、目标配置或生命周期错误。没有留记录就回看提交,不用示例错误冒充真实日志。
03|P0:你是开发组长,带几个人?怎么分工?
为什么会问:简历写了职级,正文却全是个人技术产出,面试官需要确认头衔和实际职责是否对得上。这题答不上来,会连带影响前面所有技术内容的可信度。
回答框架:先给事实(组内人数、你负责的范围、向谁汇报),再讲实际做的事,最后说明自己现在还写多少代码。技术岗组长通常仍是主力开发,说清楚这一点是加分不是减分。
技术岗组长常见的职责边界,挑你真正做的讲:
- 任务拆分与排期:把工具适配这类批量工作拆成可并行的单元,定义完成标准(编译通过?功能验证?还是设备实测?)
- 技术方案定稿:适配路线选交叉编译、剥离 UI 保留内核,还是重新实现——判断依据是依赖树和平台耦合面
- 代码评审与规范:评审谁做、卡什么。你的项目里天然该卡的是资源释放、错误处理路径、构建可复现
- 对外协作:跟上游、测试、发布的接口
最有力的讲法:把简历里「沉淀 CMake Toolchain、Native 模块模板和依赖适配清单」直接讲成组长工作的产物——把重复动作模板化,让第二个人做同类工具时更快。这是能落到实处的技术影响范围,比报人数有说服力。
边界:
- 如果你的组长偏「技术负责人」而不管绩效招聘,主动说清。技术岗这样很正常,讲明白比含糊强。
- 如果带组是任职中途开始的,如实讲从什么时候开始,不要让人以为两年都在带。
- 不要把「参与团队完成 20+ 工具」讲成「我带队完成 20+」,也不要把使用现成规范讲成自己制定。这两条在下一题同样适用。
04|P0:简历写了熟练使用 Claude Code / Codex / Cursor,代码是你写的还是 AI 写的?
为什么会问:这是把 AI 工具写进简历的必然代价。提问动机通常不是好奇,是核实基本功。答不好,前面所有技术内容都会被打折。
答题主线:把工具定位成加速重复劳动,把判断权留在自己这边。结构是——用它做什么、不用它做什么、怎么保证产出质量。
一个可落地的说法:
- 用:批量适配中的重复动作(构建脚本、模板代码、依赖清单整理)、查文档与定位源码、测试用例骨架
- 不用:架构与所有权设计、并发与生命周期的关键路径、涉及安全的部分。这些必须自己想清楚,因为出问题时要能定位
- 质量:产出一律走编译、单元测试和评审,不能解释的代码不合入
最强的证明方式:举一个工具帮不上忙的问题。比如终端客户端里回调注册与连接建立的时序竞争,或者 PyTorch 加载阶段的符号与链接路径问题——这类要读日志、看 ELF、判断构建配置,工具给不出答案。把这个讲清楚,问题自然就回答了。
边界:不要贬低工具装清高,面试官可能自己也在用;也不要把 skill 体系吹成产品。你实际做的是自建跨工具 skill 与项目级规范,本质跟沉淀 CMake Toolchain 模板是同一件事:把重复动作固化下来。
可以反问:团队现在怎么用这类工具、有没有规范。这是个正常且显成熟的问题。
二、CrossShellNext:核心项目深挖
05|P0:为什么分协议层、NAPI 层和 UI 层?
参考回答:协议层处理连接、读写、协议错误和会话状态;桥接层处理参数转换、异步结果交付及跨语言生命周期;UI 层处理展示和用户操作。这样的边界减少协议实现对界面平台的依赖,也便于单独验证通信行为。
向下是用户操作转换为会话命令,向上是数据、状态或错误转换为界面事件。工作线程不应直接操作界面对象。桥接层需要做参数检查和错误转换,但不应把 SSH/SFTP 的主体实现堆进去。
追问:为什么不移植 PuTTY? 应回答实际目标平台集成与维护成本,不说“PuTTY 只能跑 Windows”。它有 Unix 版本。PuTTY 官方 FAQ
实际经历边界:分层有利于独立测试,不等于你已经做过脱离 NAPI 的全套单测;线程池也不一定是 napi_async_work 自带执行器,必须按项目实现说明。
06|P0:Session 抽象、工厂和单例分别解决什么?
答案:Session 抽象稳定的使用方式,让上层不必知道每种协议的底层句柄;工厂根据协议类型和配置创建合适实现;管理器维护会话身份、查找和生命周期。抽象应围绕真正共有的能力,而不是为了使用设计模式。
SSH、串口、SFTP 并非所有操作相同。终端 resize 与文件传输可以做成独立能力接口,或显式返回不支持,避免设计一个充满空实现的大基类。新增协议通常还要处理工厂注册、配置校验、UI 能力展示和测试,并不一定“只加一个子类”。
单例的取舍:全局入口简单,但隐藏依赖、跨测试残留状态、退出顺序都更难管理。可注入的管理器更易测试。不要把“线程安全的单例初始化”误说成“管理器里所有方法都线程安全”。
07|P0:阻塞 I/O 放进线程池,就不会卡了吗?
答案:只能说明不直接占用 UI 线程,不能保证任务及时执行。若固定 N 个线程都被长时间读取占住,后续发送、连接或关闭任务可能一直排队,这是线程池饥饿。
需要先分清短任务和长连接读循环。少量会话可以采用专用 I/O 线程,成本是线程和栈资源;大量连接更适合非阻塞 I/O 与事件通知,工作池处理有限时长的计算或协议任务。无论选什么,都要设计超时、取消和队列上限。
追问:如何确认? 记录任务排队时间、执行时间、活跃线程数和队列长度。制造多个不返回数据的会话,再执行发送/断开,看控制操作能否及时完成。不能只观察“UI 没卡”就说并发模型没问题。
个人回答:先解释项目究竟由谁读、谁写、谁关、是否共享池,再讨论改进。不要临时把已有线程池项目说成 epoll 架构。
08|P0:atomic 与 mutex 各保护什么?
答案:atomic 保证针对该原子对象的操作按相应语义执行;mutex 用来保护一组共享数据与它们之间的不变量。会话常有“状态、句柄、任务、回调是否有效”等关联信息,单独把状态改成原子变量不等于整个会话线程安全。
例如线程 A 读到 Connected,线程 B 随后关闭句柄,A 再使用句柄仍可能出错。解决方案可以是受锁保护的状态和资源获取、任务持有稳定所有权、串行化操作或代际标识,取决于架构。
不建议在持有状态锁时进行漫长网络 I/O、等待线程退出或回调未知代码。通常在锁内检查并确定操作权限,在锁外执行耗时操作,再按生命周期规则提交结果。
追问:CAS 可以用于状态转换,但不能天然解决资源回收。也不能说 atomic 一定无锁、mutex 一定比 atomic 慢;是否无锁与类型、平台和实现有关。
09|P0:关闭窗口时,后台还在读,会怎样安全退出?
设计答案,不宣称是现有实现:先让会话进入 Closing 并拒绝新任务,再使本次会话的回调失效或标记代次已结束;向 I/O 执行单元发出取消,使用既定的唤醒/超时机制让阻塞操作返回;等待正在使用资源的任务结束;最后按依赖顺序释放协议对象、连接和桥接资源。
这里有三个重点。第一,不能只删管理器里的指针,队列中的任务可能仍捕获原始地址。第二,不能拿着任务结束时也需要的锁去 join,否则形成死锁。第三,已经入队的回调仍可能执行,接收端必须识别已关闭会话,数据对象也要有人负责释放。
不能假定“另一个线程 close(fd) 后所有阻塞 I/O 都立刻安全退出”,还要考虑平台行为和 fd 数字复用。最好由明确的 I/O 所有者执行最终关闭,并验证取消流程。
应测:连接中关闭、回调已排队时关闭、反复快速开关、网络超时与用户关闭同时发生、重复关闭。按你实际代码说明其中哪些已测,哪些是建议。
10|P0:为什么 weak_ptr 不能单独解决所有过期回调?
答案:weak_ptr 不延长对象生命,回调执行时 lock 成功才能得到一个暂时有效的 shared_ptr,可以避免对象已经析构后的访问。但“对象还活着”不等于“操作仍合法”。会话可能已经关闭,或者对象复用后已是下一次连接。
因此还要校验生命周期状态或连接代次。回调携带会话 ID 与 generation,消费前确认仍是当前连接;关闭后丢弃不再有效的结果。检查和后续操作要处于一致的同步规则内,不能检查完就失去保护。
shared_ptr 捕获能延长生命,但可能导致关闭后迟迟不释放;对象持有回调、回调又强持有对象还会形成环。正确做法是先明确所有权,再选择弱引用、取消标记或任务上下文,而不是所有地方都改成 shared_ptr。
11|P0:NAPI 跨线程回调要注意什么?
答案:一般 Native 工作线程先生成独立持有的数据,使用允许跨线程的投递机制,把需要访问 JS 值的工作交给允许操作 JS 的线程。要明确数据何时转移所有权,投递失败、队列满、运行时关闭时由谁释放。
线程安全函数还涉及生产者持有/释放、关闭状态、终结清理。不能在已关闭后继续投递,也不能因为投递接口“线程安全”就认为业务对象安全。队列无界会在消费慢时持续增长;有界队列则需要定义阻塞、拒绝或合并策略,界面线程不能因等待自身消费而死锁。
Node-API 文档可帮助理解这些概念,但你使用的是 OpenHarmony NAPI,具体接口支持、销毁和调度语义必须核对项目 SDK,不能直接把 Node.js 的所有行为套过去。Node-API 线程安全函数说明
12|P0:回调「先缓存后投递」解决什么问题?缓存会不会一直涨?
问题背景:C++ 侧建立连接和 JS 侧注册回调是两个独立的时间点。连接先完成时,早期产生的数据——SSH 欢迎横幅、认证提示、shell 的第一个提示符——已经到达,但还没有接收方。直接丢弃的表现是终端首屏缺内容,用户看到一个空白窗口。
做法:回调就绪之前,把这段数据按会话缓存;注册完成后按原顺序补投,然后切换到直投路径。要说清三件事——缓存的粒度(按会话还是全局)、切换的判定条件(什么算回调已就绪)、以及切换过程中新到的数据怎么不乱序。
必然的追问
- 缓存无上限吗? 对端在注册前持续输出的话,缓存会增长。需要有上限策略:到达容量后丢弃最旧、合并,或者反压到读取侧。如果当前实现没设上限,就如实说「按会话缓存,没有设上限,实际量级是连接初期的少量输出」,不要临场编一个上限值。
- 缓存期间会话就断开了呢? 缓存属于会话资源,会话销毁时必须一起释放,否则就是泄漏。这跟安全关闭、过期回调是同一类生命周期问题。
- 顺序怎么保证? 补投和直投之间要有明确交接点,通常在同一把锁或同一个队列里完成切换。不能补投还没结束就开始直投,否则会乱序。
- 为什么不让 C++ 等 JS 注册完再连? 可以,但那样会把建连延迟绑定到界面就绪,UI 初始化慢时会拖慢连接,也让 Native 层反过来依赖上层时序。缓存方案把这个耦合留在自己这一侧。
讲法:这是你实际做过的设计,落到「当时的现象是什么」比讲模式名有力得多——首屏少了提示符或横幅,是个能让人立刻理解的具体故障。如果具体的上限、锁的位置记不清了,回看代码再答。
13|P0:SFTP 浏览与传输为什么分开?channel 和 session 一样吗?
答案:长文件传输可能占住同一串行处理路径,导致目录浏览延迟。分离处理路径可以降低这种相互影响,但要弄清分的是任务队列、SFTP 句柄、SSH channel,还是两条独立 SSH 连接。
多个 channel 可以复用一个 SSH 连接,因此仍共享传输层和底层 session;两个独立 SSH 连接隔离程度更高,但增加握手、认证和资源开销。库对象能否被并发访问要按所用版本及 API 约束处理,不能认为“两个句柄”就没有共享状态。
面试讲法:先说清 v4 所写“独立会话”到底是什么,再说改善的是交互请求等待,不是网络带宽翻倍或所有资源完全隔离。没有测速数据,就不报加速倍数。排查时分别观察队列等待、远端响应和传输耗时。
14|P0:超时、断线和自动重连怎么做?
答案:把 DNS、TCP 建连、SSH 握手、认证和业务操作分阶段设置超时,同时控制整体截止时间。计算持续时间用单调时钟,避免系统时间调整影响判断。TCP 连上不等于 SSH 已认证,read 暂时没数据也不等于断线。
非阻塞 TCP connect 返回 EINPROGRESS 后等待可写,随后读取 SO_ERROR 判定成功或失败,不能“可写就当成功”。超时后停止本次尝试,并按资源所有权规则清理。Linux connect 手册
重连可采用有上限的指数退避和随机抖动,但认证失败、主机密钥变化不应无限自动重试。连接恢复也不等于业务可重放:远端命令可能已执行,重新发送会重复产生副作用。文件续传要核对目标内容与偏移。
易错点:用户长时间不输入的终端可以完全健康;需要有定义的探测响应或操作超时,不能仅按“多久没读到字符”强制断开。项目没做自动重连就如实说未实现。
15|P0:心跳线程和连接、断开操作会不会打架?
为什么会问:简历写了「atomic 管状态、mutex 协调连接与断开,配合心跳线程」。心跳是一个独立的、周期性访问会话的线程,它天然会跟用户发起的关闭操作竞争,这是个很自然的深挖点。
三层要说清
- 为什么需要应用层心跳:NAT 和中间设备会回收长时间无流量的连接。终端会话经常长时间没有输入,看起来「卡住」,其实连接已被静默丢弃。TCP 自带的 keepalive 默认间隔很长(Linux 默认约 2 小时起),而且是内核行为、粒度粗,不能替代应用层探测。tcp(7)
- 心跳与关闭的竞争:心跳线程醒来时,会话可能正在关闭甚至已经释放。所以访问会话前要先检查状态并持有保护,而且检查和使用必须在同一段同步区间内——检查完就放开锁再去用,等于没检查。这跟安全关闭、过期回调是同一类问题。
- 库的并发约束:libssh2 这类库的对象通常不允许多线程同时调用。心跳发送和业务读写如果都直接操作同一个 session,必须串行化。常见做法是心跳线程只设置一个标志,由 I/O 线程实际发送;或者统一走同一把锁。
追问:几次探测失败算断线?失败要区分「发送失败」和「对端没回应」,前者往往立刻能判定,后者要等超时。重连策略见上一题。
边界:说清你用的是库自带的 keepalive 配置还是自己起的线程,两者的失败处理位置不同。没做过多次探测判定就如实说只做了单次探测或只做了保活发送。
16|P0:SSH 加密了,为什么还需要检查主机密钥?
答案:加密只解决和当前对端的通信保密,主机身份验证用于确认这个对端是不是目标服务器。若首次连接或密钥变化时直接忽略验证,可能与冒充服务器建立加密连接。
实现上需要保存或使用可信的主机公钥记录,首次连接明确确认指纹,后续比较;密钥变化不能静默覆盖。密码和私钥不写日志,私钥存储、权限与使用过程需要限制暴露。Telnet 本身不提供 SSH 等价的保密认证,应限制使用环境。
追问:密钥生成是用户认证能力,不能替代服务器主机密钥检查。面试要分清“验证服务器”和“服务器验证用户”两个方向。
17|P0:本地、远程、动态端口转发分别是什么?动态转发怎么实现?
答案
- 本地转发:客户端 listen 一个本地端口,收到连接后通过 SSH 请求服务端连到目标地址,然后双向搬数据。目标地址在建立转发时就固定了。
- 远程转发:请求服务端在它那一侧监听,服务端收到连接后通知客户端,由客户端连到本地目标。方向与本地转发相反。
- 动态转发:本地监听的端口上跑 SOCKS 协议(通常 SOCKS5)。目标地址不是预先配置的,而是从每个客户端请求里解析出来,再逐个开通道。
所以动态转发比前两种多一层:要处理 SOCKS 握手——认证方法协商、请求解析、地址类型(IPv4 / IPv6 / 域名)、应答码——之后才是通道搬运。这一层通常要自己写,通道本身由库提供。
必然的追问
- 一个转发端口来多个连接怎么办? 每个 accept 到的连接开一条独立通道,通道生命周期跟这条 TCP 连接绑定,不能复用。
- 半关闭怎么处理? 一侧 EOF 不代表另一侧也结束。要分方向处理关闭,否则会截断数据或提前销毁通道。这是转发实现里最容易出错的地方。
- 背压呢? SSH 通道有窗口,本地 socket 也有缓冲。一侧慢时不能无限读另一侧,否则内存持续增长。
- 这些通道跑在哪个线程? 跟你的线程池和会话模型怎么配合,是不是每条转发占一个线程。
边界:讲清楚哪些是你实现的、哪些是库提供的通道能力。SOCKS 解析这层值得展开,是能体现协议实现能力的部分。半关闭和背压如果没做完整处理,如实说。
三、C++ 与并发基础
18|P0:RAII、unique_ptr、shared_ptr 怎么选?
答案:RAII 把资源生命周期绑定到对象生命周期,资源可以是内存、文件描述符或库句柄。默认优先使用值对象或 unique_ptr 表示唯一所有者;只有多个参与者确实要共同维持生命时才使用 shared_ptr。自定义删除器要调用对应库的释放函数,而不是统一 delete。
shared_ptr 的引用计数管理与被管理对象的线程安全是两回事。不同 shared_ptr 实例可共享控制块并各自操作,但对象内部并发读写仍需同步;同一个 shared_ptr 变量被并发修改也需要适当同步。weak_ptr 用于观察而不强持有。C++ 标准草案 shared_ptr
项目连接:Session 若由管理器唯一拥有,异步任务如何保证使用期间不析构?若用 shared_ptr 延长生命,关闭是否仍能及时完成?面试官关注所有权设计,而不是选择哪种指针“更高级”。
19|P0:自定义 deleter 怎么写?对 unique_ptr 有什么影响?
答案:unique_ptr<T, D> 的删除器类型是模板参数的一部分,会影响对象大小。无状态的仿函数或 lambda 类型通常能被空基类优化掉,指针大小不变;用函数指针当删除器则要多存一个指针。shared_ptr 把删除器存在控制块里、不进类型,所以类型统一,代价是控制块分配。
库句柄的释放函数签名往往对不上(返回 int、参数不是 T*),所以要包一层转调。
释放顺序是重点:libssh2 的 channel 要在 session 之前释放,knownhosts 同理。不能靠成员声明顺序碰运气——要么显式定义析构顺序,要么用嵌套的所有权结构把依赖表达出来,让编译器保证。这是三类句柄放在一起时最容易出事的地方。
容易被追问的点
- 释放函数可能失败,但 deleter 跑在析构路径上不能抛异常,失败只能记录
- 空句柄要判断。很多库对 NULL 释放是安全的,但不能假定,要查文档
- 同一句柄不能被两个 owner 管理,包装类要禁用复制、实现移动,防止重复释放(见移动语义那题)
项目讲法:你封装的是 session / channel / knownhosts 三类,讲清它们的依赖关系和释放顺序,比说「我用了 RAII」有分量得多。再补一句异常路径——认证失败、握手中途失败时资源同样被释放,这才是自定义 deleter 相比手写 cleanup 的实际收益。
20|P0:std::move 真会移动吗?移动构造为什么写 noexcept?
答案:std::move 本质上进行值类别转换,使后续重载决议有机会选择移动操作;它本身不搬资源。对象没有可用移动构造时可能发生复制,const 对象转为右值后也通常不能绑定到常规的非 const 移动参数。
移动后状态遵循类型契约;标准库对象通常处于有效但未指定状态,不能一概认定变空或变 null。资源类需要明确移动后源对象不再释放已转移资源。
容器扩容为了满足异常保证,可能在移动可能抛出且复制可用时选择复制。正确标注 noexcept 可以使安全移动更容易被选用,但函数真抛异常会导致终止,不能为了性能随便标。
项目追问:自定义 fd/libssh2 包装类一般禁用复制、实现移动、防止重复释放;能用现成 RAII 类型组合就优先遵循 Rule of Zero。
21|P0:vector、map、unordered_map 什么时候用?迭代器会失效吗?
答案:vector 连续存储,随机访问和顺序遍历友好,尾部插入均摊常数复杂度;中间插删要移动元素。map 提供有序查找,通常对数复杂度;unordered_map 平均常数查找,但最坏可退化,不能笼统说永远 O(1)。
vector 扩容会使原有元素指针、引用和迭代器失效,erase 也影响被删位置及其之后;unordered_map rehash 会使迭代器失效,但通常不使元素引用/指针失效,删除该元素则当然失效。使用时按具体容器操作契约判断。
项目连接:会话表遍历期间删除、异步任务捕获容器元素地址、增加会话触发扩容,都是可能的悬空访问来源。reserve 可以减少重分配,不能替代线程安全或生命周期管理。
22|P0:条件变量为什么一定要检查条件?如何关闭队列?
答案:条件变量不是“记住一次通知”的信号计数器。真正的条件应保存在受锁保护的共享状态里,例如队列非空或停止标志。等待者使用带谓词的 wait,在持锁状态下判断,不满足则释放锁并等待,被唤醒后重新拿锁检查,处理虚假唤醒及其他消费者抢先取走任务。
生产者在同一同步规则下入队并更新状态,然后通知。消费者取任务后释放锁再执行,避免任务执行期间阻塞其他入队/出队。
关闭时在锁内设置 stopped 并拒绝新任务,notify_all 唤醒等待者,再等待工作线程结束。需要提前定义“处理完已有任务再退出”还是“取消排队任务”,不能让调用方的 future 永久等待。析构时不能让工作线程 join 自己,也不能持有工作线程退出需要的锁。
误区:用 sleep 反复轮询通常既延迟又难保证正确;只添加 notify_all 而不改变谓词,等待者可能马上继续睡。
23|P0:data race、内存序、volatile 有什么区别?
答案:两个线程对同一内存位置发生未正确同步的冲突访问,其中至少一个是非原子写,可能构成数据竞争并导致未定义行为。不是“多数时候读到旧值”这么简单。volatile 不提供线程间同步,不能用来实现可靠的停止标志。C++ 标准草案:数据竞争
relaxed 提供该原子对象的原子访问,不自动发布旁边的普通数据。典型单次发布场景中,生产者先初始化数据,再 release 写就绪标志;消费者 acquire 读到对应发布值后,可见发布前的写入。但重复写入、对象销毁和多轮复用仍需额外设计。
实际选择:先用能证明正确的锁或默认原子语义,再基于测量优化。面试不必声称项目做了 lock-free;能解释为什么当前方案不需要它更重要。CAS 循环也可能面临 ABA、饥饿和安全回收问题。
24|P0:怎么定位死锁?怎样预防?
答案:先区分真死锁、长时间 I/O、线程池饥饿和正常等待。获取各线程堆栈,确认每个线程等待什么资源、该资源由谁持有,寻找环形等待。一次快照只能提供线索,必要时多次采样看是否有进展。
预防上建立固定锁顺序,缩短临界区,避免持锁调用外部回调或进行长 I/O;需要同时锁多个互斥量时可以使用合适的多锁机制,但它不能替代对整体调用关系的分析。递归锁不解决跨线程循环等待。
项目场景:UI 关闭会话时持有会话锁等待工作线程退出,而工作线程退出前也要拿会话锁更新状态,就会互相等。解决关键是调整关闭协议与锁范围,不是简单增加超时时间或改大线程池。
25|P1:C++17 特性怎样结合项目讲?
答案:optional 表示可能不存在的值,例如配置查询结果,区别于“空字符串也是合法结果”;string_view 是不拥有数据的视图,可减少不必要复制,但不能指向已经释放的临时字符串;结构化绑定适合解包 map 元素或返回值;if constexpr 用于编译期分支,未选分支在相应模板实例化条件下不会按普通运行时分支处理。
项目联系:异步任务捕获 string_view 特别危险,投递后源字符串可能已销毁,跨线程传递通常需要拥有数据或明确共享生命。不能为了展示新语法牺牲所有权清晰性。
简历写 C++17 不表示每个特性都在项目用过。面试应选一两个确实用过的例子;没用过的可解释原理,但别编使用场景。
四、Linux、网络和定位问题
26|P0:TCP 为什么会“粘包”?读写该怎么处理?
答案:TCP 提供有序字节流,不保留应用 send 的消息边界。一次发送可能分多次收到,多次发送也可能合并收到,这不是网络把数据弄坏,而是应用需要自己定义报文边界。
常见做法是固定头部包含负载长度,接收缓冲保存剩余字节,头不够继续等,头够则校验长度,负载不够继续等,完整后消费一帧并循环解析下一帧。必须限制最大长度,防止整数溢出与恶意内存申请,明确字节序。
send 可能只发送部分数据,要保存偏移继续发送;非阻塞 EAGAIN 表示暂时不能继续,不是连接必然断开。EINTR 按操作语义处理;正常的非零长度 TCP recv 返回 0 表示对端有序关闭发送方向,不代表所有本地待发送数据已经处理完。
边界:SSH/SFTP 库已处理其协议封装时,不要又在它的通道数据外随意加自定义帧。终端输出本来可能就是字节流。
27|P0:select/poll/epoll、LT/ET 怎么选?
答案:这些机制通知哪些描述符可能进行 I/O。select/poll 每次处理传入的描述符集合;epoll 在内核维护关注项和就绪项,适合很多连接而部分活跃的情况,但不保证所有小规模任务都更快。
LT 在条件仍满足时可以继续通知。ET 通常配合非阻塞描述符,处理到 EAGAIN 再等待新事件,否则缓冲里仍有数据却可能没有新边沿。写入要维护待发送缓冲,避免一直监听可写导致忙循环。Linux epoll 手册
追问:ET 连续读取也要考虑公平性,不能让一个大量输出的连接一直占住事件线程。可以有工作预算和重新调度机制,但要确保仍有待处理数据的连接不会被遗忘。简历只有线程池,不要把这些学习知识说成现有 epoll 实现。
28|P0:进程、线程、共享内存与“零拷贝”怎么理解?
答案:同一进程内线程共享地址空间,各自有栈与执行状态;不同进程通常隔离地址空间,通过 IPC 交换数据。共享内存允许不同进程映射同一存储,但映射地址可能不同,跨进程结构不能直接传本进程的普通指针,常使用偏移、索引或受管理的句柄。
共享内存不自动解决同步。必须定义谁写谁读、数据何时发布、读者结束前能否复用缓冲、进程崩溃后谁回收。跨进程原子和同步原语还要满足平台、初始化与共享属性要求,不是把普通 std::mutex 放进去就万事大吉。
零拷贝边界:减少哪一段拷贝必须说清,应用间不拷贝不代表设备 DMA、编解码或网络发送都不拷贝。你的简历没有共享内存组件,回答应定位为理解与方案设计,不是项目成绩。
29|P0:C++ 程序崩溃,如何定位?
答案:先保留能匹配的可执行文件、动态库、调试符号和构建版本,再看日志、core 或可复现输入。先确认崩溃线程、信号和调用栈,检查参数、对象生命周期、越界和并发访问;崩在 free 不一定是 free 的问题,可能早已写坏堆。
在隔离测试环境可用 gdb ./app core,结合 bt full、info threads、thread apply all bt 查看栈与线程;优化构建可能导致变量不可见或栈不完整。交叉平台 core 还要匹配目标架构和库,不能拿宿主系统同名库解释地址。GDB 线程调试文档
能复现时结合 AddressSanitizer/UndefinedBehaviorSanitizer 检查内存和未定义行为,竞态可在支持环境用 ThreadSanitizer 辅助。它们有开销和覆盖限制,未报错不等于没有 bug。core 可能含密钥和用户数据,不随意上传;线上 attach 会影响进程,需遵循授权与运维流程。
30|P0:CPU 高、卡顿、内存上涨分别怎么查?
答案:先定义症状与负载:同样连接数、输入和运行时长下复现。CPU 高看线程热点、循环、频繁拷贝、锁竞争和队列自旋;CPU 不高但慢,应看网络、磁盘、锁等待、调度和任务排队。只看 on-CPU 热点可能看不到等待时间。
内存增长要区分对象泄漏、缓存、队列积压、分配器保留和映射。RSS 上升不直接等于泄漏。对终端项目尤其要看 UI 消费是否跟得上输出,历史缓冲、回调载荷与连接关闭后资源是否仍被持有。
优化前后固定环境和输入,记录延迟分位数、吞吐、CPU、峰值内存或任务等待等与问题相关的指标。没测量不能把“用了线程池”写成“显著提升性能”。不会 perf 可以先补本地演示,但不要虚报生产使用经验。
31|P0:动态库找不到、符号找不到、架构错,怎么区分?
答案:库文件找不到先查部署位置、依赖链和搜索路径;undefined symbol 说明加载或链接时无法解析符号,常见原因是库版本不对、依赖遗漏、符号可见性或 ABI 差异;架构/解释器不匹配则先查 ELF 头与 program interpreter。
可用 file、readelf -h -l -d -Ws 等检查目标文件。不要在宿主机直接运行来历不明目标的 ldd;它也不能可靠替代目标环境加载验证。先看依赖项,再到受控的目标环境确认。
glibc 下 RPATH 与 RUNPATH 的搜索优先级、对间接依赖的作用不同;RUNPATH 一般只用于直接依赖的搜索。$ORIGIN 可用于相对库位置,但会受加载器规则约束。musl/OpenHarmony 不能默认照搬 glibc 的全部规则。Linux 动态加载器手册
五、升级子系统
32|P0:差分升级和连续升级需要校验什么?
项目开场:我在工具侧计算并写入目标版本校验信息,端侧读取升级包并扩展校验流程,支持差分与连续升级场景。
展开答案:差分转换通常依赖特定基线,不能把适用于 A→B 的补丁随便应用到其他版本。连续升级还要确保上一轮实际结果能作为下一轮基线,不能只看界面版本号。
需要分清传输包的哈希、输入镜像/文件的哈希、应用差分后目标内容的哈希,以及承载它们的元数据。包未损坏不等于目标写入正确;目标版本字符串匹配也不等于底层内容匹配。
必须按代码解释的点:你的 SHA256 究竟覆盖哪个文件或字节区间,何时计算,存在哪个字段,端侧在哪个阶段读取和比较,失败如何退出。简历“目标版本哈希”过于宽泛,不能由我代替选择成某个镜像或分区。一般流程理解可以完整讲,但不要声称自己实现了整套升级框架。
33|P0:SHA256 能防止恶意篡改和回滚吗?
答案:单独的哈希不证明来源。如果攻击者能同时替换包和附带哈希,校验仍可能通过。需要把可信哈希纳入认证过的元数据或签名链,验证信任来源;防回滚还需要可信版本策略、受保护的版本状态等机制。
完整性、真实性、防回滚是不同目标。下载完整不等于来自授权发布者,签名有效也不一定说明该旧版本被当前策略允许安装。
与你的经历对应:可以说你扩展了哈希校验链路;如果签名、密钥和防回滚由既有框架处理,就说明边界。不能把“计算 SHA256”升级成“设计安全升级体系”。面试官问整个流程时,可分析应该有哪些环节,但标明哪些不在个人改动范围。
34|P0:升级中途断电如何保证恢复?
设计答案:先识别最危险的写入阶段,再考虑可恢复的旧版本、写入日志/检查点、双槽或其他恢复入口。状态记录不能早于对应数据可靠落盘,启动流程需要辨别未完成升级而不是盲目把“开始写入”当作“已完成”。
A/B 是一种方案,不是所有设备都采用。即便是 A/B,也要考虑启动成功确认、重试次数、共享数据兼容和失败回退。单分区更新更依赖恢复系统或其他可验证的恢复策略。
测试:在读取包、校验、写数据、更新元数据、切换启动状态等阶段注入失败,重启确认系统处于可解释状态。测试环境必须允许恢复,不能对真实业务设备随意断电。
个人边界:v4 没写你实现过断电恢复,应该回答“我的改动在校验/刷写模块;整体恢复机制需要结合项目启动与升级设计说明”,再讲自己的理解。
35|P0:为什么使用 EROFS?与 ext4 有什么取舍?
答案:EROFS 面向只读文件系统,可配合压缩适合分发静态镜像;ext4 支持常规读写,适用于需要更新内容的场景。是否替换取决于分区是否只读、目标内核支持、镜像生成工具和性能要求,不是 EROFS 在任何情况下都更好。Linux EROFS 文档
压缩通常以额外解压成本换取更少存储和读取数据量,实际结果与数据可压缩性、缓存和设备速度有关。LZ4 是项目选择,不能不看构建配置就说所有 EROFS 都默认使用它。
兼容挂载追问:说明文件系统类型如何获知或识别、失败如何处理、旧镜像是否仍能使用、目标内核是否具备支持。不能靠“第一次失败就换类型一直试”掩盖损坏或参数错误。收益没有测试数据时就说结构与部署方面的变化,不报百分比。
36|P0:镜像从 32MB/128MB 到 20KB,分区也变小了吗?
答案:不一定。分区是存储上的容量范围;镜像文件是要交付或写入的内容表示;压缩包、稀疏镜像、文件逻辑长度和实际占用也不是同一指标。取消按分区容量填充,可以减少包内镜像的大小,却不一定改变分区布局或设备预留空间。
比较时使用同一内容、同一文件类型、明确是否压缩,并记录逻辑大小与实际存储占用。不能用压缩后的空镜像和未压缩的满分区镜像比较,然后宣称通用压缩率。
个人回答:v4 已恢复这个数字,简历口径是「升级包中 patch 空镜像由 32MB / 128MB 降至 20KB」。按这个口径答:比较的对象是升级包里那个 patch 镜像文件,不是分区容量。32MB 和 128MB 是解耦前按两种分区规格填充出来的大小,20KB 是解耦后按实际内容生成的。
要能解释两件事:「空」镜像里剩下的是什么(文件系统必要的元数据结构),以及为什么解耦前必须按分区大小填充。不能顺势宣称设备存储占用、下载流量或升级耗时同比例下降——那需要另外的数据。数字对不上时以你能找到的产物为准,不要为了好看倒推。
注意:v4 已删除「压缩效率较 ext4 提升 50%」这条,因为口径最难解释。被问到 EROFS 的收益时讲机制和取舍(见上一题),不要主动报这个百分比。
37|P0:C++ 接口迁移到 Rust,是不是 FFI?
答案:两种情况不同。把一段功能改为 Rust 实现、同时调整调用链,不一定存在运行时跨语言调用;如果 C++ 与 Rust 在同一程序内互调,才需要解释边界协议、ABI、数据布局与资源所有权。
通用 FFI 设计通常使用明确的 C ABI、适合跨边界的数据表示和显式释放函数,避免直接暴露 C++ STL 容器或 Rust String/Vec 的内部布局。字符串传指针与长度时要说明编码、借用时长和谁释放,错误要转成双方能理解的形式。
异常或 panic 能否跨边界有明确约束,不能默认允许穿透。回调还要说明线程、安全条件与 context 生命周期。
项目边界:v4 只写接口代码从 C++ 迁移到 Rust,因此先说明你属于哪一种,再回答用的实际接口。不能看到两门语言就补上 cbindgen、extern C 或零拷贝成绩。
追问「精简代码 1k+ 行」:v4 恢复了这个数字,要能说清删的是什么。重构解耦通常同时发生三件事——删掉重复或废弃的逻辑、把一部分实现换到另一侧、以及净减少。如果 1k+ 行主要是前两类,就说「接口层重构后 C++ 侧净减少 1k+ 行,对应逻辑迁到 Rust 实现」,不要让人理解成整体代码量凭空少了 1k 行。找不到可靠的统计口径时,改说「精简了重复的接口层代码」,不报具体行数。
38|P0:升级和并发模块如何写有意义的单元测试?
答案:先把文件访问、刷写设备、协议读写等外部依赖从核心判断中分离,用可控替身模拟成功、错误、短读、超时和中途失败。验证的不只是返回码,还包括失败后资源是否释放、状态是否允许重试、是否错误地继续写入。
升级可测缺字段、损坏包、基线不匹配、校验失败、文件系统识别失败;会话可测建连中取消、重复关闭、过期回调、队列满和读失败。并发测试用明确的同步点控制交错,例如让任务停在“将要回调”再触发关闭,不依赖“sleep 一会应该碰到”。
单测替代不了真实设备集成测试和长期压力测试。没有统计就不写覆盖率;使用 gtest 本身不等于稳定性已证明。现场选一个真实用例说明输入、预期、观察点和回归价值。
六、PyTorch 与大型 Native 工程移植
39|P0:交叉编译只需要设置 —target 吗?
答案:不够。还要匹配目标 sysroot、头文件、链接器、C/C++ 运行库、架构选项与依赖库。CMake Toolchain 用于描述目标环境和查找策略;“编译器生成 ARM64 指令”不能保证其链接到正确 libc 或运行时。
交叉构建还必须区分宿主工具和目标产物。构建阶段需要运行的代码生成器应能在宿主执行,最终库则要面向目标。把目标版生成器拿到宿主执行,或让依赖查找误用宿主库,都是常见失败来源。CMake 交叉工具链文档
排查顺序:查看真实编译/链接命令、include 和 library 搜索位置、目标 ELF、依赖版本;保持干净构建目录,避免旧缓存掩盖配置变化。try_run 这类需要执行目标程序的检测,要有目标运行环境或经过验证的交叉配置,不能随便把检测结果全部设成成功。
40|P0:glibc 与 musl 都支持 Linux,为什么不能直接复用二进制?
答案:源代码接口相近不等于二进制兼容。动态加载器、符号及其版本、库扩展行为和依赖生态可能不同,编译时链接到 glibc 的二进制不能因为 CPU 一样就认为可在 musl 上直接运行。
我会先确定错误发生在编译、链接、加载还是运行。编译阶段检查不可用 API 或条件宏;链接阶段检查目标库和符号;加载阶段看 interpreter、依赖和符号版本;运行阶段再定位语义差异。C++ 还要同时考虑标准库及其 ABI,不能全部归因于 libc。
项目讲法:选一个你真实改过的 API/配置举例,解释为什么不能只复制一个 so。兼容层可能帮助部分程序,不代表目标 PyTorch 的全部依赖被支持。也不能把标准 musl Linux 的结果直接等同于所有 OpenHarmony 环境。
41|P0:你写的适配层(shim)具体屏蔽了什么?
为什么会问:简历写了「编写适配层处理 glibc / musl 系统接口差异」。这是你的实际产出物,面试官会想知道里面到底有什么,而不是听一个名词。
回答结构:先分类,再举一两个真实例子,最后说边界。
shim 层通常处理三类差异:
- 缺失或行为不同的接口:某些 GNU 扩展在 musl 上没有,或者同名函数的边界行为不一致(返回值约定、错误码、缓冲区处理)。做法是提供等价实现或包装转调。
- 头文件与宏的差异:特性检测宏、类型定义、条件编译分支。构建期就要拦住,不能等运行时。
- 符号与链接期问题:符号版本、弱符号、依赖引入的间接符号。这一类往往不是写代码解决,而是调整链接参数或依赖来源。
必然的追问
- shim 里的实现和原生实现语义完全一致吗? 通常不完全一致。要说清你实现的是被实际调用到的那部分行为,而不是完整语义。
- 怎么确认没漏? 构建通过不等于运行正确。要讲你的验证手段——设备上跑的用例覆盖到哪些路径,哪些路径没覆盖。
- 有没有更好的办法? 能通过升级依赖版本或改构建配置解决的,优先于写 shim。写 shim 是最后手段,因为它会成为长期维护负担。
边界:只讲你真改过的接口,能说出名字和现象最好。不要把「适配层」讲成一个通用兼容框架——它通常是若干个针对性的补丁集合。也不要把标准 musl Linux 的验证结果直接等同于所有目标设备环境。
42|P0:CPython 扩展 ABI、文件后缀和 wheel 标签是什么关系?
答案:Python 扩展除了 CPU 架构,还与所用 Python ABI、动态库依赖和平台有关。普通 CPython 扩展不应假定可跨任意次版本加载;Limited API/Stable ABI 是有约束的另一套承诺,不能默认 PyTorch 的扩展符合。CPython C API 稳定性
扩展后缀影响导入器识别,实际值应查询目标解释器配置而不是硬编码 .cpython-312.so。wheel 的 Python、ABI、平台标签用于表达支持范围,musllinux 标签还包含 musl 版本与架构要求;改文件名不会修复真实的不兼容。PEP 656
追问:主模块可加载,Python 包仍可能缺导入时需要的代码或资源,所以还要测试干净安装。PyTorch 项目应说明最终是 wheel 安装、设备原生包分发,还是组合方案,不能把 hnpcli 分发自动等同于符合通用 wheel 规范。
43|P0:import torch 成功,为什么还不能说移植完成?
答案:import 只覆盖初始化和一部分依赖加载,很多算子、后端或运行路径在真正调用时才被触发。应分层验证:加载 → 基础 Tensor 运算 → 目标模型所需算子 → 模型运行 → 保存加载或交付链路 → 结果正确性与资源消耗。
你的简历已有 Tensor、torch.nn、autograd、trace 和 TorchScript 验证,可以讲实际通过的用例,但不能据此推出全部算子、全部模型或完整训练能力可用。
性能追问:没有做同环境对比就回答“当前主要验证构建、加载与功能;还没有形成可靠性能结论”。若补测,需要固定模型、输入、线程、精度、设备状态,区分首次加载、预热和稳态,比较输出误差、耗时与峰值内存。未经测量不能说“性能无回退”。
44|P0:面向 CPU 推理的移植,为什么还验证 autograd?
答案:面向推理交付不等于运行时不含梯度能力。autograd 是 PyTorch 的核心机制之一,构建里保留它很正常;把它纳入验证,是在确认这部分功能在目标平台上可用,不是在声称跑通了完整训练链路。这两件事要分清。
eval 影响模块的训练/评估行为(Dropout、BatchNorm 等),但不自动关闭梯度记录;no_grad 控制对应范围内的梯度记录;inference_mode 进一步减少跟踪开销,但由它产生的张量后续用于 autograd 有额外限制。PyTorch Autograd 机制
个人回答:v4 简历已不写「裁剪 CUDA、分布式与训练相关模块」,只写面向 aarch64-musl 设备的 CPU 推理需求完成构建、加载与分发适配。被问到构建裁剪时,按实际情况区分三类——哪些构建选项确实关掉了、哪些只是没打包、哪些只是没测到。三者不能混为一谈。
没有未裁剪的同配置基线,就不报体积缩减率。v4 也已删除「libtorch_cpu.so 约 180MB」,因为单个产物大小不能证明优化效果。
七、按目标岗位选学的分支
45|P1:Qt 的线程归属与信号槽有什么坑?
答案:QObject 有线程归属,事件处理依赖对象所在线程的事件循环;GUI 对象通常只能在主线程使用。耗时任务放工作线程,结果通过合适的跨线程通信回到界面,不在后台直接操作控件。
Direct Connection 在发出信号的线程直接调用槽;Queued Connection 把调用排入接收者线程事件队列。Auto Connection 根据发射时所处线程与接收者线程判断,不是只看 sender 对象的线程归属。QThread 对象自身通常属于创建它的线程,不能把它和运行的工作线程混为一谈。Qt Threads and QObjects
与你的关系:剥离过 Qt 界面不自动证明熟练开发 Qt 桌面应用。遇到 Qt 主岗,先准备一个你实际接触过的对象生命周期或跨线程场景;没有经验就说明主要强项在 Native 层。
46|P1:智驾中间件岗位问 DDS、共享内存,你怎么回答?
答案:先明确自己没有量产 DDS 经验。可以从需求拆解:谁发布什么类型的数据、多少订阅者、跨进程还是跨机器、能否丢旧数据、最大允许延迟、消费者变慢怎么办,然后再选通信方案。
DDS 的发布订阅和服务质量配置需要结合具体实现验证。可靠传输不是无限缓存,保留全部历史也需要资源限制;最新传感器画面和必须确认的控制指令,积压策略可能不同。共享内存侧重点是样本所有权、发布可见性、读者退出与复用时机。
迁移能力怎么说:“我做过多会话状态、异步交互和目标平台适配,这些方法可以迁移,但 DDS/QNX 的具体 API 和量产要求需要补齐。”避免说 SSH 客户端就是智驾中间件;也不要把学习中的零拷贝方案写成简历成果。
47|P1:后台岗问数据库和高并发,你的项目够吗?
答案:网络客户端与后台服务共享连接、协议、并发和资源管理基础,但后台还要处理请求容量、数据一致性、服务故障与部署运维。先承认边界,再说明能迁移的能力。
若设计一个简单任务服务,应先确定请求是否可重试、任务状态如何持久化、重复请求怎样识别、执行失败是否重试以及如何限制队列和并发。幂等不是“接口多调用几次不报错”,而是重复请求不产生额外的业务副作用,通常依赖业务标识与原子状态更新。
数据库题按所用产品展开:索引服务于实际查询,写入有维护成本;事务的保证取决于隔离级别与访问方式,不是开事务就消除所有并发问题。没有数据库项目,不应补上“熟悉 MySQL/Redis”来掩盖缺口。本轮只建议作为次要投递方向。
48|P1:三维地层剖切和性能优化如何讲出技术含量?
答案:按数据处理链讲:先用空间包围结构筛选可能与平面相交的对象或三角形,再做平面与三角形相交计算,整理交线或轮廓,最后处理剖切面的三角化和渲染。BVH 用来减少候选测试,不是直接完成所有剖切几何。
需要考虑共面、边顶点恰好落在平面上、浮点误差、重复交点、轮廓连接和孔洞。普通 Delaunay 三角剖分不自动保证复杂边界与孔洞正确,应说明你项目实际采用的约束、过滤或输入限制。
性能上区分模型下载/解析、CPU 几何计算、绘制调用和 GPU 开销。LOD、视锥剔除和 shader 优化分别针对不同环节,不能说用了某技术就一定稳定多少帧。它能体现计算与定位能力,但不能改写为原生 OpenGL 引擎经历。
八、四道手写题:解法、边界与测试
以下给出面试解题答案,不声称已实现或测试。练习时自己写 C++17 代码,再运行边界测试;不需要一上来搭一个大项目。
49|P0:实现可关闭的有界阻塞队列
接口与约定:容量必须大于零;push 在满时等待,关闭后返回失败;pop 在空时等待,关闭后可取完已入队数据,取尽返回结束。明确选择“关闭后排空”,不要中途改成丢弃模式。
实现答案:使用一个互斥锁、一条容器队列、not_empty 与 not_full 两个条件变量、closed 标志。push 等待 closed || size < capacity,醒来先检查 closed,再入队并通知消费者。pop 等待 closed || !empty;为空且关闭才结束,否则出队后通知生产者。close 在锁内改标志,再唤醒两侧等待者。
正确性要点:谓词与队列状态由同一把锁保护;关闭能唤醒已在等待的生产者;执行任务不在队列锁内;拥有队列的对象销毁前必须确保没有其他线程还在访问它。元素复制/移动抛异常时也要保证容器和锁状态合理,不能只测试 int 就声称对所有类型强异常安全。
复杂度:使用 deque 时常见入出队操作为常数级,空间 O(capacity),阻塞等待时长不属于算法常数复杂度。
必测:容量 1、空队列关闭、满队列关闭、重复 close、多生产者/消费者不丢不重、关闭后 push 失败、已有数据排空、等待中的线程都能结束。
50|P0:实现 LRU 缓存
实现答案:双向链表保存最近使用顺序,哈希表保存 key 到链表节点迭代器。get 命中把节点移到表头并返回值;put 已存在则更新并移到表头;新键插入表头,超容量时删表尾并同步删除哈希表项。
为什么两种结构:哈希表平均常数查找,链表常数时间移动和删除已知节点。只用 vector 会有移动开销,只用链表查找慢。复杂度为平均 O(1),哈希冲突情况下不能承诺最坏常数;空间 O(capacity)。
边界:容量 0、更新已有键不能误增 size、淘汰时两结构一致。若 API 返回引用,要考虑随后淘汰导致引用失效;返回值或受控句柄更易解释。基础版不是线程安全,加入锁后也需处理返回对象的生命。
必测:put(A)、put(B)、get(A)、put(C) 后 B 被淘汰;重复更新 A;不存在键;容量 0/1;多次命中不增加节点。异常安全深入追问时,说明表与链表更新失败如何回滚。
51|P0:解析四字节长度前缀的 TCP 消息
协议约定:前四字节为网络字节序的无符号负载长度;约定是否允许零长度、最大帧长和非法报文的处理方式。这里是练习协议,不是你的 SSH 实现。
实现答案:把新字节追加到接收缓冲,从读位置开始处理。剩余不足四字节则等待;足够时逐字节或用安全方式读取长度并转换字节序,检查最大值及长度加头部是否溢出;负载未齐则等待,齐则交付一帧并继续解析。消费后适时整理缓冲,避免每处理一帧都搬动全部剩余字节。
生命与限制:回调若异步使用负载,必须复制或转移缓冲所有权,不能传即将被复用的 view。限制总缓冲大小并设置合理的接收超时,避免对方只报大长度却长期不发完整数据。
必测:头部拆成四次输入、一帧拆多次、两帧一次到达、头与下一帧部分混合、零长度、超长、断开时残帧、连续小帧。实现得当时总解析工作接近 O(总字节数),不能因每次 erase 头部而退化为大量搬移。
52|P0:实现异步连接状态机,防止过期连接结果覆盖新连接
状态约定:Idle、Connecting、Connected、Closing、Closed;失败可统一转 Closed 并携带错误,或单独定义 Error。关键是明确允许哪些事件,而不是状态名字越多越好。
实现答案:每次新连接分配一个递增 generation。异步建连任务捕获 generation,完成后在统一同步规则下检查仍匹配且处于 Connecting,才可提交 Connected 与资源;否则按任务所有权释放本次结果。关闭先转 Closing、取消当前 generation 的有效性并拒绝新业务,等待本代任务按约定退出,再提交 Closed。
状态和资源提交必须一致,不能先发布 Connected,句柄却尚未可用。仅比较 generation 不能解决裸指针悬空,任务上下文还需要稳定生命周期。若禁止同一对象重连,也可以通过每次创建新会话对象简化设计。
必测:第一次慢连接、关闭、第二次连接成功、第一次结果才返回;连接成功与关闭同时触发;关闭两次;任务失败后资源归还;对象销毁时任务仍在执行。检查最终状态、资源释放次数以及是否收到过期成功回调。
九、如何把这些答案练成自己的
最先练的十题
01 自我介绍、03 带人与职责、07 线程池、08 同步边界、09 安全关闭、12 先缓存后投递、19 自定义 deleter、26 TCP 粘包、31 动态库与符号、36 20KB 口径。
选这十题的理由:01 和 03 每场都会用到;07/08/09/12/19 是 CrossShellNext 的完整追问链(谁执行 I/O → 谁保护什么 → 怎么安全退出 → 早期数据怎么办 → 资源怎么释放);26 和 31 是简历新增 socket 与动态库加载后必然被问的两点;36 是简历上最扎眼的数字。
第二梯队(有面试邀请再补):04 AI 工具、15 心跳竞争、17 端口转发、32 升级校验、39 交叉编译、41 shim 层、43 验证边界。
60 分钟模拟面试
| 时间 | 内容 | 自查标准 |
|---|---|---|
| 0—5 分钟 | 自我介绍、职级与个人贡献 | 不把总开发年限说成 C++ 年限;组长职责与 20+ 工具的口径要一致 |
| 5—20 分钟 | CrossShellNext 创建、收发、关闭、错误链路 | 能讲清谁持有对象、谁执行 I/O、谁最后释放 |
| 20—30 分钟 | 升级或 PyTorch 挑一个深挖 | 至少一处真实代码改动、一个验证方法、一个边界 |
| 30—40 分钟 | C++、Linux、网络基础 | 先说成立条件,再给反例,不只背定义 |
| 40—55 分钟 | 四道手写题抽一道 | 先约定接口,写核心逻辑,最后主动测边界 |
| 55—60 分钟 | 反问团队与工作 | 问清实际业务、技术栈、交付责任与派驻安排 |
面试时可以直接问公司的问题
- 这个岗位主要做 Linux 用户态应用、设备中间件,还是内核驱动?新人的前两项任务是什么?
- C++ 在整体工程里占多少,是否需要承担 Qt/Windows 或车载协议开发?
- 当前最难的问题是功能交付、稳定性还是性能?如何验证发布质量?
- 岗位是甲方自有编制还是派驻/外包?工作地点和服务项目是否一致,项目调整时岗位如何安排?
准备材料,而不是继续填几十个事实模板
只整理四类材料即可:当前项目的关键提交/模块位置;一个真实问题的复现与修复记录;一个能说明结果的测试或产物;真实雇主、时间与负责范围。不要带出公司保密代码或内部数据。公开仓库可以定位公开提交,内部项目用脱敏说明。
针对 v4 简历,这四类材料建议具体落到:
| 材料 | 建议对应 |
|---|---|
| 关键提交 / 模块位置 | flashd 相关改动;终端客户端的会话与回调模块 |
| 真实问题的复现与修复 | 回调注册与连接建立的时序竞争(第 12 题),或 PyTorch 加载阶段的符号/路径问题(第 31、41 题) |
| 能说明结果的产物 | 解耦前后的 patch 镜像文件(第 36 题的 20KB);gtest 用例 |
| 雇主与负责范围 | 德科信息 2024.03 - 至今、开发组长;派驻项目变化的时间点;20+ 工具中本人主责的清单 |
这四类材料足够把大多数追问落到事实。掌握不了的内容先不写进简历;确实掌握的技能也不需要因为没有公开链接就删掉。