Appearance
科大讯飞 C++ 技术一面复盘
面试官提问清单
本次材料是语音自动转写。以下只保留能够由上下文稳定确认的技术问题,并对明显的术语识别错误作了校正;不保留身份信息、原始对话和具体项目标识。
项目经历与工程取舍
- 项目是个人项目、团队项目还是企业项目?团队如何分工,你具体负责哪些模块?
- 多路直播客户端为什么从现成播放后端切换为 FFmpeg?切换后增加了哪些工程复杂度?
- 直播地址如何获得,FFmpeg 如何拉流?项目以什么方式集成 FFmpeg 动态库?
- 音视频同步使用什么时钟?输入流的 PTS 在重连或网络波动后跳变时如何处理?
- 扫码登录的完整流程是什么?登录态保存哪些信息,为什么会失效?失效后能否自动恢复?
- 热点片段如何根据弹幕密度和在线规模识别?片段如何包含触发前的内容?
- 热点持续时间过长时,如何避免内存和磁盘被写满?录制数据最终保存在哪里?
- 客户端是否还依赖服务端?
- 时序建模项目中承担了哪些工作?为什么选择 LSTM 和 Transformer 类模型?
- 是否修改过模型结构?训练使用什么硬件,训练停止条件是什么?
- PyTorch 模型如何导出为 ONNX,又如何验证 ONNX Runtime 推理结果?
- 客户端中的 ASR 是本地推理还是远程 API?具体模型、参数规模、量化方式和推理后端是什么?
- 本地 ASR 占用多少显存?它与多路视频硬解码是否争抢 GPU 资源?普通用户没有独立显卡时怎么办?
动态库、链接与 CMake
- 动态库有哪些加载方式?除
dlopen外还有什么方式? - 如何编译静态库和动态库?CMake 中分别使用哪些命令?
- 从源码开始,CMake 配置、构建、链接和安装的基本流程是什么?
add_library、add_executable、target_link_libraries分别解决什么问题?PRIVATE表示什么?- 构建动态库时
-fPIC有什么作用? LD_LIBRARY_PATH有什么作用?- Linux 下如何查看共享库导出的动态符号?
ldd能否完成这件事?
C++ 运行期、多线程与调试
- 哪些异常场景会导致 C++ 程序在运行中崩溃?
- 是否有多线程开发经验?项目中的解码、播放和录制线程如何划分?
- 是否使用过线程池?线程池由哪些组件构成,有什么优势?
- “线程不安全”具体是什么意思?
- C++ 中常见的锁和配套 RAII 封装有哪些?
- C++ 程序发生内存泄漏时,可以使用哪些工具定位?
- 是否使用过 Valgrind?它与 ASan/LSan 的适用边界有什么不同?
- Core dump 保存了什么信息,有什么作用?
- 如何使用 GDB 调试 core 文件?查看调用栈、全部线程和局部变量的命令是什么?
Linux、网络与基础设施
- 平时使用哪些 Linux 发行版?
- Ubuntu 下如何更新软件包索引?软件源配置文件在哪里?
- 如何查找僵尸进程?僵尸进程能否直接被
kill清理? - 如何查看进程?如何按进程名查 PID?
top -p接收的是名称还是 PID? - 操作系统为什么区分用户态和内核态?哪些操作会进入内核?
- TCP、HTTP 和 WebSocket 分别位于哪一层?
- WebSocket 的 HTTP Upgrade 握手过程和关键请求头是什么?
- 是否使用过 Wireshark 等抓包工具?排查“两台机器无法连接”时应观察哪些报文?
- 是否使用过 Docker、Dockerfile 和 Docker Compose?是否接触过 Kubernetes?
- 是否使用过消息中间件?对消息队列了解多少?
- Redis 用在什么场景?该项目是否真的需要 Redis?
开发方式与反问
- 项目开发时是否使用 AI 编程工具?主要用在哪些环节?
- AI 生成代码约占多少,架构决策、代码审查和验证由谁负责?
- 对岗位的业务、C++ 使用范围和其他主要开发语言有什么想了解的?
项目题知识回顾与参考答案
FFmpeg 时钟、时间基与断流重连
PTS 是帧的展示时间戳,DTS 是解码时间戳;二者只有结合流的 time_base 才有时间意义。不同流的时间戳不能直接比较,应先通过 av_rescale_q 等方式换算到统一时间基。
直播播放通常选择一个主时钟:有稳定音频时常以音频时钟为主,无音频或低延迟监看场景可采用单调递增的外部时钟。视频根据主时钟决定等待、展示或丢帧,不能简单地让音视频线程各自按收到帧的速度播放。
断流重连后,上游可能更换时间戳起点,PTS 可能回退或大幅跳跃。处理链路应为:
text
检测断流或时间戳不连续
→ 停止旧 epoch 的投递
→ flush 解码器、重采样器和待播放队列
→ 为新流建立新的时间戳基准/偏移
→ 把新 PTS 映射到本地单调时间轴
→ 音视频重新同步后恢复播放录制若采用 remux,还要分别规范化各流起点,保证写给 muxer 的 DTS 满足单调性,并按时间戳交错写入。只描述“改用本地时钟”仍不够,需要说明输入时间戳如何重映射、旧队列如何清理,以及播放与录制是否共享同一个时钟策略。
热点检测、预录与容量上限
弹幕条数必须按时间窗口和在线规模归一化,否则大房间天然更容易超过固定阈值。可使用滑动窗口统计单位时间弹幕数、活跃用户数或增长率,再以在线人数归一化,并通过进入阈值、退出阈值和冷却时间抑制抖动。
触发前片段适合保存为固定容量的编码包环形缓冲区。编码包通常远小于解码帧;达到预录时长后覆盖最旧数据,内存天然有上界。触发后把预录包与新增包持续写入分片文件,而不是把整个热点一直堆在内存中。还应显式定义:
- 单片最大时长或大小;
- 单次热点最大录制时长;
- 总磁盘配额和最低剩余空间;
- 写盘失败、磁盘满和程序退出时的收尾策略;
- 关键帧边界与容器封装完整性。
“假设热点不会太长”不是容量设计。正确回答要给出有界缓冲、持续落盘、硬性时长/空间限制和监控指标。
扫码登录与登录态失效
通用扫码登录状态机是:申请二维码标识和地址、展示二维码、有限频率轮询“未扫描/已扫描待确认/成功/过期”、成功后安全持久化会话、停止轮询。二维码过期、用户取消和网络错误应是显式状态。
Cookie 可能因服务端过期策略、用户主动退出、密码或安全设置变化、设备数量限制、风控和服务端轮换而失效,不能只归因于“登录设备太多”。客户端收到明确的未认证响应后,应清除失效状态并要求重新认证。只有服务端正式提供 refresh token 或续期接口时才能自动刷新;不能伪造“自动重新登录”。
回答这类问题时不应背诵 Cookie 字段名,而应说明状态机、失效检测、安全存储、敏感日志边界、取消与重试策略。
时序模型、ONNX 导出与验证
模型选择应与任务和数据对应:
| 问题 | 可选模型 | 回答重点 |
|---|---|---|
| 短到中等长度的序列分类 | LSTM/GRU | 顺序归纳偏置强、数据量较小时易训练,但长依赖和并行性受限 |
| 长序列预测 | PatchTST 等 patch-based Transformer | patch 降低有效序列长度,注意力建模较长依赖,并行训练较好 |
| 状态预测回归 | LSTM 或 Transformer | 明确预测窗口、损失函数、MAE/RMSE 和滚动预测误差 |
| 模式识别分类 | 序列编码器 + 分类头 | 明确类别不平衡、混淆矩阵、精确率/召回率/F1 |
“参考论文做了微调”无法说明技术贡献。至少应说清输入特征、窗口/patch 大小、隐藏维度、输出头、损失函数、对照基线、验证集划分和指标变化。
ONNX 部署的完整验证链路是:模型切换到 eval(),准备代表性输入,选择支持算子的 opset,按业务需要声明动态 batch/序列维,导出后运行 onnx.checker,再用相同输入比较 PyTorch 与 ONNX Runtime 的输出误差。最后还要测预热后延迟、吞吐、峰值内存和真实数据指标,不能以“文件导出成功”代替部署验证。
本地 ASR 的工程回答
介绍本地模型时应给出一条可复现的配置,而不是在模型名称和参数量之间犹豫:
text
模型仓库/精确版本
→ 参数规模
→ 权重量化格式与位宽
→ 推理后端及提交版本
→ 音频采样率和分块策略
→ GPU/CPU、峰值显存、实时率 RTF
→ 测试集上的字错率或人工抽检结果多路视频硬解码主要占用专用视频解码单元并消耗显存/带宽,ASR 推理主要占用 CUDA 计算单元、显存和内存带宽;两者仍可能相互影响。应通过显存预算、推理队列、并发上限和端到端延迟实测决定是否能同时开启。无可用 GPU 时应明确“不支持、改用 CPU 模型或由用户关闭功能”,而不是声称无成本运行。
动态库、CMake 与 ELF
动态库的两类加载方式
- 装载期动态链接:构建可执行文件时链接共享库,ELF 记录
DT_NEEDED;程序启动时由动态装载器查找并映射库,符号可立即或延迟绑定。源码中像调用普通函数一样调用库接口。 - 运行期显式加载:程序调用
dlopen打开指定共享库,使用dlsym查符号,结束后dlclose。适合插件、可选后端和按能力加载。Windows 对应LoadLibrary、GetProcAddress、FreeLibrary。
只回答 dlopen 漏掉了更常见的装载期链接。显式加载还必须检查 dlerror(),定义稳定 ABI,并规定对象由哪一侧创建和销毁。
CMake 最小目标化流程
cmake
cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)
add_library(core STATIC src/core.cpp)
target_include_directories(core PUBLIC include)
add_library(plugin SHARED src/plugin.cpp)
target_link_libraries(plugin PRIVATE core)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE plugin)bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build -j
ctest --test-dir build --output-on-failure
cmake --install build --prefix /desired/prefixadd_library(... STATIC/SHARED ...) 创建库目标,add_executable 创建可执行目标,target_link_libraries 声明目标之间的链接依赖。PRIVATE、PUBLIC、INTERFACE 描述“使用要求是否向依赖者传播”,不是“动态链接还是静态链接”的开关。
-fPIC、运行期搜索路径与符号表
-fPIC 生成位置无关代码,使机器指令不依赖固定装载地址,适合共享库被映射到不同虚拟地址,并减少对只读代码段的运行期重定位。把非 PIC 目标文件链接进共享库时,常会出现重定位错误或不利于共享的 text relocation。
LD_LIBRARY_PATH 是 ELF 动态装载器的运行期搜索目录之一,常用于临时测试或诊断。正式部署更适合使用正确的 install layout、RUNPATH/$ORIGIN 或系统 ldconfig 配置,避免全局环境变量造成版本污染。
查看导出动态符号可使用:
bash
nm -D --defined-only libexample.so | c++filt
readelf --dyn-syms --wide libexample.so
objdump -T libexample.soldd libexample.so 查看的是共享库依赖及其解析路径,不是导出符号表。两类问题必须分开。
C++ 运行期、多线程与调试
哪些情况会导致崩溃
典型原因包括非法内存访问、越界、use-after-free、double free、栈溢出、堆元数据破坏、除零/非法指令等致命信号,以及下列 C++ 终止路径:
- 异常无人捕获;
- 异常逃出
noexcept函数; - 栈展开期间析构函数再次抛异常;
- 可 join 的
std::thread在析构时仍未join/detach; - 断言失败或代码显式调用
abort/terminate; - 数据竞争触发未定义行为。
“空指针和越界”只覆盖了最直观的一部分。回答时应按内存错误、异常终止、并发未定义行为和主动终止四类组织。
线程安全与线程池
线程安全是指操作在允许的并发调用下仍保持数据竞争自由、对象不变量和接口语义正确。它不等同于“使用了锁”:不可变对象、线程封闭、原子操作和正确设计的无锁结构也可以线程安全;加错锁同样不安全。
线程池通常包含工作线程、任务队列、唤醒机制、停止协议和背压/拒绝策略。其主要价值是:
- 复用线程,摊薄创建和销毁成本;
- 限制并发度,避免线程数量随任务无界增长;
- 统一排队、调度、取消、异常和生命周期;
- 暴露队列长度、排队时间、活跃线程和拒绝数等指标。
CPU 密集型线程数通常从可用核心数附近开始压测;I/O 密集型可以更多,但仍受下游容量、内存、上下文切换和尾延迟约束。任务队列也必须有界,否则线程数受控但内存仍可能被排队任务写满。
C++ 常见同步原语包括 std::mutex、std::recursive_mutex、std::timed_mutex、std::shared_mutex 和基于 std::atomic_flag 的自旋锁。代码中优先用 std::lock_guard、std::unique_lock、std::scoped_lock、std::shared_lock 管理锁生命周期。条件变量是等待/通知机制,不应直接列为“一种锁”。
内存泄漏、Sanitizer、Valgrind 与 core
可复现的内存错误优先在带符号测试构建中运行 ASan/LSan:
bash
c++ -g -O1 -fno-omit-frame-pointer \
-fsanitize=address,undefined source.cpp -o app
ASAN_OPTIONS=detect_leaks=1 ./appValgrind Memcheck 无需编译器插桩即可检查泄漏和非法访问,适合某些已有 Linux 二进制,但运行开销通常明显高于原程序。ASan 擅长越界和 use-after-free,LSan 专门报告退出时仍可追踪的泄漏;常驻缓存增长、队列积压和真正失去引用的内存要通过堆 profile、指标与所有权分析区分。
Core dump 不只保存调用栈,还可包含寄存器、线程、映射和部分进程内存。常用命令是:
bash
gdb ./app core
(gdb) bt
(gdb) thread apply all bt
(gdb) frame 3
(gdb) info locals
(gdb) p variable分析必须使用与 core 完全匹配的可执行文件和调试符号。更多细节见 C++ 调试、Core Dump 与运行期诊断。
Linux、网络与基础设施
软件源、进程与僵尸进程
sudo apt update 刷新软件包索引;sudo apt upgrade 才更新已安装软件。APT 源通常配置在 /etc/apt/sources.list 和 /etc/apt/sources.list.d/,不能把“更新索引”和“修改软件源”混为一件事。
bash
ps -eo pid,ppid,state,stat,cmd
pgrep -a process_name
pidof process_name
top -p PID僵尸进程的状态为 Z:子进程已经退出,只剩退出状态等待父进程调用 wait/waitpid 回收。僵尸本身已经不执行,不能靠向它发送普通信号清理;应定位父进程,修复其 SIGCHLD/回收逻辑,必要时谨慎重启父进程。top -p 接收 PID;按名称查询优先用 pgrep -a,而不是把 top -p 当名称过滤。
用户态、内核态与系统调用
用户态限制应用执行特权指令和直接操作硬件、页表等资源;内核态提供受控的资源管理和隔离。系统调用、异常和中断都可能使 CPU 进入内核,但用户态/内核态切换不等于一定发生线程切换。
read、write、openat、mmap、socket、connect 等是系统调用接口。C/C++ 标准库函数可能只是用户态逻辑,也可能经过缓冲后再调用系统调用;例如 printf 不是“一次调用必然对应一次 write”。
WebSocket 分层与握手
TCP 位于传输层,HTTP 和 WebSocket 位于应用层。WebSocket 通常先通过 HTTP/1.1 请求升级:
http
GET /channel HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <随机 Base64 值>
Sec-WebSocket-Version: 13服务端接受后返回 101 Switching Protocols,并携带 Upgrade、Connection 和根据 key 计算的 Sec-WebSocket-Accept。随后连接传输 WebSocket 二进制帧,支持全双工通信和 ping/pong/close 控制帧;客户端发往服务端的帧必须掩码。
回答只提 Upgrade 和 101 还不完整,至少要说明 Connection: Upgrade、key/accept 校验和协议切换后的帧语义。
抓包排障的证据链
排查“连接不到对端”时先固定源/目的 IP、端口、网络命名空间和路径,再在正确接口抓包:
- 是否完成 ARP/邻居发现,路由是否选对接口;
- 是否发出 TCP SYN;
- 对端是否返回 SYN-ACK、RST 或 ICMP 错误;
- 是否持续重传但无返回,提示中间防火墙/NAT/路由丢弃;
- 若握手完成,应用是否立刻 FIN/RST,TLS/HTTP 是否返回错误。
“确认没连到本机、排除网络问题”缺少可验证证据。应明确抓到的 flags、五元组、重传或响应方向,以及最终定位到路由、防火墙、端口监听还是虚拟化网络映射。
Docker、消息队列与 Redis
Docker 回答应区分:Dockerfile 定义镜像构建步骤,docker build 生成镜像,Compose 描述多容器服务、网络、卷和配置;Kubernetes 负责更大规模的编排、期望状态、调度和服务治理。没使用过 Kubernetes 可以直接说明,但应掌握 Pod、Deployment、Service 的职责边界。
消息队列的面试回答可从“为什么用”展开:异步解耦、削峰填谷、失败重试和跨服务事件传播;随后说明投递语义、幂等消费、顺序、积压、死信、消费位点和可观测性。仅说“看过文章”无法证明掌握。参见 分片、分布式事务与消息队列。
Redis 只有在能说明数据模型和收益时才应引入,例如热点缓存、会话/短期状态、计数、限流或协调。一个小型单实例网站若已有 CDN 且源站压力很低,放弃 Redis 可能正是正确取舍;但表达应是“测得瓶颈不在数据访问,因此删除该依赖”,而不是先声称使用、再说项目完全不需要。参见 Redis 基础与持久化。
本次回答中需要改进的地方
高优先级:明确答错或没有答出
| 问题 | 本次表现 | 改进方向 |
|---|---|---|
| 动态库加载方式 | 只答出 dlopen | 补齐装载期动态链接与运行期显式加载,并说明各自使用场景 |
| CMake 目标命令 | 混淆 add_library、target_link_libraries 和 PRIVATE | 用最小 CMakeLists 逐个解释目标创建、链接和使用要求传播 |
-fPIC | 未答出 | 记住“位置无关代码、共享库、重定位与代码页共享”四个关键词 |
| 导出符号表 | 回答 ldd | 改为 nm -D、readelf --dyn-syms 或 objdump -T;ldd 只看依赖 |
| GDB 调试 core | 未答出具体命令 | 熟练复述 gdb binary core、bt、thread apply all bt、frame、info locals |
| 查僵尸和按名称找进程 | 把 top -p、ps 等命令混在一起 | 用 ps -eo ... state 查 Z,用 pgrep -a name 查名称,top -p 只接 PID |
中优先级:方向基本正确但缺少边界
- FFmpeg 时钟:识别到了 PTS 跳变,也提到本地时钟,但没有讲统一时间基、epoch 重置、队列 flush、PTS 重映射和录制 DTS 单调性。
- 热点录制:先用“热点不会持续太久”作为假设,追问后才补上分片和上限。应第一句就回答“固定容量预录环 + 触发后持续写盘 + 时长/磁盘硬上限”。
- 登录失效:把原因猜成设备数量上限,证据不足。应区分服务端过期、撤销、风控和正式刷新机制,并说明如何根据状态码处理。
- 模型项目:只说参考论文修改,无法体现自己做了什么。应准备一张“输入—结构—损失—数据划分—基线—指标—部署误差”表。
- ASR 部署:模型名称、参数量和后端表述反复修正,显存也只有大致数字。应保留一条由脚本打印的精确环境、量化格式、峰值显存、RTF 和识别质量记录。
- 线程池:只说限制线程数和统一管理,漏掉线程复用、任务队列、唤醒、背压、关闭协议、异常传播和指标。
- 线程安全:用“没有加锁”定义线程不安全不准确。回答应落到数据竞争、对象不变量和并发语义,锁只是实现手段之一。
- 崩溃场景:答出了空指针、越界和析构期间二次异常,但组织较散;应按非法内存、异常终止、并发 UB、主动终止分类。
- 内存泄漏:想到了 ASan,但工具名称和能力边界没有说清,也未主动给出 LSan、Valgrind 和 heap profiler。
- 用户态/内核态:保护系统资源的方向正确,但把 C 库函数和系统调用混为一谈。应举
read/openat/mmap/socket,并说明标准库可能有用户态缓冲。 - WebSocket:答出了 HTTP Upgrade 和 101,但漏了 key/accept、版本、帧、掩码和 ping/pong/close。
- 抓包案例:只描述“排除网络问题”,没有陈述 SYN、SYN-ACK/RST、重传、接口和最终根因,无法体现抓包能力。
表达与项目可信度
- 项目介绍过长,背景、功能和个人贡献混在一起。建议采用“目标与约束 20 秒 → 架构 30 秒 → 个人负责 30 秒 → 一个最难问题及量化结果 60 秒”的顺序。
- 多次使用“相当于”“大概”“好像”“应该”等填充词,尤其在模型、显存、命令和参数上削弱可信度。无法确认时直接说“这个命令我现在记不准”,随后给出已确认的概念边界。
- 回答工具使用不应只报工具名。最小证据链应包括问题现象、观察手段、关键输出、根因、修改和回归验证。
- 对 Redis、消息队列和 Kubernetes,没有实践就如实说明;随后给出自己掌握的最小知识边界,不要为了覆盖简历关键词而描述一个自己也认为没有必要的使用场景。
- AI 编程占比不是核心。更好的回答是:人负责需求、架构和验收标准;AI 生成局部实现;每次改动经过代码审查、构建、测试、性能与资源验证;不熟悉的 GPU/QML 模块尤其需要以真实运行证据闭环。
- 反问只问了岗位做什么,方向合理但可以更具体:C++ 主要位于推理引擎、协议解析还是基础设施;新人前三个月负责什么;主要性能指标和线上调试链路是什么。可参考 技术一面结束后的反问清单。