面灵AI→

恒生电子 C 与 C++ 开发工程师面经(10.10)

时间
2026-10
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 选一个项目,介绍一下你认为做得最好的功能,是怎么实现的,实现思路是怎样的?
  3. 如果说你用 C 去写一个通信库,你认为怎么写才能让这个库最高效运行?
  4. TCP 你了解吗,讲一讲。
  5. TCP 的三次握手是怎么做的?ACK 起到什么作用?
  6. 你有用过 Linux 吗,是否熟悉常见命令?
  7. 进程和线程你了解吗?进程和线程的通信是如何实现的?
  8. 你提到用 AI 协作开发,你怎么确保 AI 写的代码没有问题?
  9. 小项目中或许可以,但在大的工程项目中,每个部分单独看都是好的,组合链接起来就出错了,你该如何去排查?
  10. 我们这边 base 有杭州和武汉,你更倾向于哪一边?

《参考解析》

用 C 写一个高效通信库的要点。这件事要按「模型、内存、并发、协议」四条线答。IO 模型上,高并发连接不能一个连接一个线程,要选 IO 多路复用:Linux 上以 epoll 为核心(水平触发适合简单实现、边缘触发配合非阻塞读循环性能更好),配合 Reactor 模式做「事件分发 + 回调处理」,多核再上多 Reactor(一个 acceptor 加多个 IO 线程)以分散 accept 与读写分发。内存上要避免频繁的 malloc/free 与数据拷贝:用内存池或固定大小 slab 管理小对象、用环形缓冲做收发包的粘合、必要时用 sendfile、splice 或 MSG_ZEROCOPY 做零拷贝;协议编解码要支持「半包与粘包」处理,即固定长度头加变长体的方式,读满头部再读体,绝不假设一次 recv 就是一个完整消息。并发与正确性上,给每个连接绑定固定线程避免锁竞争,用无锁队列或每线程独立缓冲做跨线程投递,谨慎使用 SO_REUSEPORT 让多进程各自 accept,并把惊群、EAGAIN 处理、SIGPIPE 忽略(改用 MSG_NOSIGNAL)、优雅关闭(半关闭状态机、TIME_WAIT 与端口复用)这些细节补齐。再往上就是工程化:定时器用时间轮管理超时连接、心跳与断线重连、限流与背压(发送缓冲水位过高就暂停读事件)、以及可观测的统计指标(连接数、QPS、延迟分位)。能说清「哪一层优化带来多少收益、代价是什么」比罗列技术名词更能体现工程判断。

TCP 与三次握手、ACK 的作用。TCP 是面向连接的可靠传输协议,用序号与确认、超时重传、滑动窗口、拥塞控制(慢启动、拥塞避免、快重传快恢复)保证可靠与流量控制,用四次挥手释放连接。三次握手是:客户端发 SYN(带上自己的初始序号 seq=x,进入 SYN_SENT);服务端回 SYN+ACK(自己的序号 seq=y,确认号 ack=x+1,进入 SYN_RCVD);客户端再回 ACK(ack=y+1,双方进入 ESTABLISHED)。为什么是三次而不是两次:一是要让服务端确认「客户端确实收到了我的 SYN」,否则一个延迟到达的旧 SYN 会让服务端单方面建立连接并分配资源;二是双方需要交换并确认彼此的初始序号,序号是后续可靠传输与去重的基础;客户端最后一次 ACK 同时也告诉服务端「你的序号我收到了」。ACK 的作用有三个:确认收到对端数据或控制报文、带回累积确认号(表示这个序号之前的数据都已收到,因此 TCP 的确认是累积的)、以及携带窗口大小做流量控制。常见追问是 SYN Flood 与半连接队列:服务端收到 SYN 后会把连接放进半连接队列,攻击者伪造大量源地址只发 SYN 不完成握手,会打满队列;对策是调大队列、开 tcp_syncookies、缩短 SYN+ACK 重传次数。另一个高频点是 TIME_WAIT 为什么要等 2MSL——保证最后那个 ACK 能到达对端(如果丢了,对端重发 FIN 时本方还能回 ACK),以及让本次连接的旧报文在网络中消散,避免影响使用相同四元组的新连接。

Linux 常用命令与进程线程通信。常用命令按用途记:文件与文本(ls -lh、find、grep -rn、awk、sed、tail -f、wc -l)、权限与用户(chmod、chown、umask)、进程与资源(ps aux、top、htop、free -h、df -h、iostat、pidstat、lsof -p)、网络(ss -lntp、netstat、ip addr、tcpdump、curl -v、ping、traceroute)、systemd 与日志(systemctl status、journalctl -u)、以及排障三件套 strace、perf、gdb。讲的时候最好带一个真实用法,比如「用 ss -lntp 确认端口有没有监听、用 lsof -p <pid> 看句柄泄漏、用 strace -p 看进程卡在哪个系统调用」。

进程与线程的通信要分开答。进程间通信(IPC)有管道与命名管道、消息队列、共享内存(最快,但需要自己做同步,通常配信号量或互斥量)、信号量、信号(异步通知,信息量小)、Socket(可跨主机,也是分布式场景的通用方式),以及 mmap 文件映射。线程间通信本质是共享同一地址空间,所以靠共享变量加同步原语实现:互斥量、条件变量(等待与唤醒)、读写锁、信号量、自旋锁、原子操作与无锁队列;volatile 在 C/C++ 里只保证不被优化掉、不保证原子性与内存序,跨线程正确性要用 std::atomic 与内存序(acquire/release)或显式屏障。一个常被追问的点是「条件变量为什么要配合谓词循环等待」——为了避免虚假唤醒与丢失唤醒;以及「析构时如何避免线程还在用对象」,需要显式的生命周期协议(停止标志、join、或者引用计数)。

AI 生成代码的验证与大型工程集成排错。验证 AI 代码要从「可执行的证据」入手,而不是读起来像对:先看它是否符合接口契约与既有风格,再补测试(单元测试覆盖边界与异常路径、必要时上 sanitizer 与静态分析)、用编译器告警与 -Wall -Werror 把低级错误挡住,关键改动自己做一遍走查并核对 diff 范围有没有越界,以及用另一个模型或同伴做交叉 review。对于并发、内存管理与错误处理这三类最容易被 AI 写错的地方,要额外用工具验证:C/C++ 上跑 ASan、TSan、Valgrind,Java 上跑并发压测与静态检查。

大工程「各部分都好的,拼起来错」这类问题,排查思路是先分清是集成期还是运行期的问题。集成期优先看构建与链接:用 nm、ldd、readelf -d 检查符号是否定义、是否有多重定义(ODR 违规)、动态库版本与 ABI 是否匹配、头文件与库是否来自同一版本;编译器与标准库版本不一致导致的 ABI 不兼容是典型元凶,做法是统一工具链、在 CI 里固定版本。运行期则要能复现再定位:先构造最小可复现样例(保留出错链路、砍掉无关模块),再用日志与调用链把故障点缩到具体边界,用 gdb 抓栈、用二分排查定位引入问题的提交;跨模块问题十有八九出在边界约定上——内存归属(谁分配谁释放)、错误码与异常语义、结构体对齐与字节序、空指针与未初始化状态、以及超时与重试语义不一致。把这些边界约定写成接口文档与集成测试,才是从根上避免「拼起来就错」的办法。

base 选择与收尾。被问更倾向杭州还是武汉,回答要基于事实与稳定性,而不是随口迁就:说明自己的实际情况(家庭、生活习惯、已有资源),并明确表达可以接受安排、能给出的到岗时间,避免含糊。这类问题在面试官心里属于「能不能来、来了能不能待住」,答案的确定性比倾向本身更重要。