文章

C/C++ 后端高频面试题系统梳理(编译、内存、并发、Linux、网络、调试、算法)

C/C++ 后端高频面试题系统梳理(编译、内存、并发、Linux、网络、调试、算法)

前言

这篇文章把 C/C++ 后端面试中常见的“原理 + 场景”题做一次体系化整理,重点是:

  • 能解释底层机制(不是只背结论)
  • 能结合项目场景(为什么这么做、带来什么收益)
  • 能延展到排障与优化(线上问题怎么定位)

1. GCC 编译四个阶段分别做了什么

典型流程:预处理 -> 编译 -> 汇编 -> 链接

  1. 预处理(cpp)
    • 处理 #include#define、条件编译
    • 删除注释,展开宏
    • 输出 .i
  2. 编译(cc1/cc1plus)
    • 词法、语法、语义分析
    • 优化并生成汇编代码
    • 输出 .s
  3. 汇编(as)
    • 将汇编指令翻译为机器码
    • 输出目标文件 .o
  4. 链接(ld)
    • 解析符号引用、合并段、重定位
    • 静态链接库/动态链接库处理
    • 生成可执行文件或共享库

常用查看:

  • gcc -E a.c -o a.i
  • gcc -S a.c -o a.s
  • gcc -c a.c -o a.o

2. 多态、虚表指针、虚函数表原理,C 如何实现多态

C++ 运行时多态

  • 含虚函数的类对象中通常有一个隐藏成员:vptr(虚表指针)
  • vptr 指向该类对应的 vtable(虚函数表)
  • 调用虚函数时通过 vptr 间接寻址,实现动态绑定

C 实现“多态”的常见方式

  • 结构体 + 函数指针表(手写 vtable)
  • 父结构体放在子结构体首部 + 向上转型
  • 通过回调实现策略切换

核心思想:数据 + 行为表解耦


3. 进程间通信(IPC)方式与应用场景

  • 管道(pipe)/命名管道(FIFO):父子进程、简单流式通信
  • 消息队列:按消息边界收发,适合异步解耦
  • 共享内存:最高性能,需配合信号量/互斥锁同步
  • 信号量:用于进程同步与互斥
  • 信号(signal):事件通知(如终止、重载配置)
  • 套接字(Unix Domain / TCP):本机或跨机通信,最通用
  • mmap:文件映射,常用于高效共享数据

选型经验:

  • 低延迟大吞吐优先共享内存
  • 跨机通信优先 socket
  • 强解耦异步优先消息队列

4. 如何减少内存碎片(内部/外部)

  • 内部碎片:分配块大于请求量(如对齐、固定块)
  • 外部碎片:空闲内存分散,无法满足大块请求

优化手段:

  • 内存池、对象池、slab(固定尺寸分级)
  • 减少频繁小对象 new/delete,可批量分配
  • 合理生命周期管理,避免长短命对象混杂
  • 周期性整理(在支持压缩的 GC 场景)
  • 分离热点对象与大对象分配策略

5. 程序性能提升(通用框架)

建议按“测量 -> 定位 -> 优化 -> 回归”闭环。

  • 算法层:降低复杂度,减少不必要拷贝
  • 数据结构层:缓存友好(连续内存、局部性)
  • 并发层:降低锁竞争,分段锁/无锁结构
  • 系统层:减少系统调用、零拷贝、异步 IO
  • 编译层:-O2/-O3,LTO,PGO
  • 资源层:连接池/线程池/内存池

6. 锁的类型、使用场景、底层原理

  • 互斥锁(mutex):独占访问,最常见
  • 读写锁(rwlock/shared_mutex):读多写少
  • 自旋锁(spinlock):临界区极短,避免睡眠切换
  • 递归锁:同线程可重入(慎用)
  • 条件变量:等待某条件成立(配合 mutex)

底层一般依赖原子指令(CAS)+ 内核 futex(竞争时睡眠/唤醒)。


7. mallocnew 区别,malloc 底层,chunk,free 如何知道大小

malloc vs new

  • malloc/free:C 风格,仅分配/释放原始内存
  • new/delete:C++,分配 + 构造 / 析构 + 释放
  • new 失败默认抛异常;malloc 返回 nullptr

malloc 底层(glibc ptmalloc)

  • 将内存按 chunk 管理
  • chunk 头部保存大小、状态位(inuse 等)
  • 有 fastbin/smallbin/largebin/tcache 等组织结构
  • 小块来自堆(brk),大块可用 mmap

free 如何知道大小

  • 通过指针前面的 chunk 元数据读取 size 字段
  • 因此 free 不需要额外传入长度

8. 字节对齐为什么更快

  • CPU 按总线宽度、缓存行读取数据
  • 对齐访问通常可一次完成
  • 非对齐可能触发额外访存或硬件修正,成本更高
  • 结构体对齐还能提升 SIMD/向量化友好性

9. 零长度数组使用场景

典型用于 C 的“变长尾部结构体”(FAM 思路):

1
2
3
4
struct packet {
    int len;
    char data[0];
};

sizeof(struct packet) + len 一次分配,减少碎片和二次指针跳转。


10. map vs unordered_mapvector 扩容、unordered_map 扩容

  • map:红黑树,O(logN),有序
  • unordered_map:哈希表,平均 O(1),无序

vector 扩容

  • 容量不足时申请更大连续空间(常见 1.5~2 倍)
  • 搬迁旧元素(拷贝/移动)
  • 旧内存释放,迭代器/引用可能失效

unordered_map 扩容

  • 桶数量增长后触发 rehash
  • 所有元素需重新映射桶位置
  • 可能产生瞬时抖动
  • 工程上可参考 Redis 渐进式 rehash 思路降低抖动

11. atomic、CAS、内存屏障、内存序

  • CAS:比较并交换,实现无锁原语
  • 内存屏障:约束编译器/CPU 重排
  • 内存序
    • memory_order_relaxed
    • acquire/release
    • acq_rel
    • seq_cst

经验:在满足正确性的前提下,尽量不用过强序(seq_cst)以换性能。


12. C++ 新特性与场景

  • auto/范围 for:提升可读性
  • 右值引用/移动语义:减少拷贝
  • 智能指针:资源自动管理
  • lambda:就地回调逻辑
  • thread/async:并发任务
  • constexpr:编译期计算
  • C++20 concept/ranges(视项目编译器支持)

13. 设计模式项目场景

  • 单例:全局配置/日志器(注意测试隔离)
  • 工厂:按配置创建不同策略对象
  • 策略:运行时切换算法(路由、限流)
  • 观察者:事件订阅分发
  • 责任链:请求过滤器链(鉴权、限流、审计)

14. 拷贝构造函数参数必须是引用吗

几乎必须是 const T&(或 T&),不能按值传参:

  • 按值会再次触发拷贝构造,导致无限递归。

15. 动态库/静态库

  • 静态库:链接时打包进可执行文件,体积大,部署简单
  • 动态库:运行时加载,节省磁盘/内存,升级灵活

16. shared_ptr 原理、线程安全性、缺陷、weak_ptr、自定义删除器

  • shared_ptr 维护控制块:强引用计数 + 弱引用计数 + 删除器
  • 引用计数增减是原子的(控制块层面线程安全)
  • 但“对象本身”并不因此线程安全

缺陷:

  • 循环引用导致泄漏
  • 额外控制块开销
  • 原子计数有性能成本

weak_ptr:打破循环引用,提供不拥有语义。

自定义删除器场景:

  • 释放方式非 delete(如 fclose, close, free, munmap
  • 对象来自对象池,需要归还池而非直接释放

17. 基类析构何时必须是虚函数

只要类会被“作为基类并通过基类指针删除派生对象”,基类析构就必须 virtual


18. 构造函数能是虚函数吗?构造/析构里能调虚函数吗?

  • 构造函数不能是虚函数(对象尚未完整建立)
  • 构造/析构期间调用虚函数,不会发生期望中的多态分派到更派生类版本(应避免依赖该行为)

19. 多重继承如何避免二义性

  • 显式作用域限定 A::func()
  • 虚继承解决菱形继承中的重复基类子对象问题
  • 接口分离,尽量组合优先于继承

20. 内存池、进程池、线程池、连接池

  • 内存池:降低分配开销和碎片
  • 进程池:隔离性强,适合多核与高可靠任务
  • 线程池:降低线程创建销毁成本
  • 连接池:复用数据库/网络连接,降低建连时延

21. 哪些场景会导致 coredump

  • 空指针/野指针解引用
  • 越界写导致内存破坏
  • 栈溢出
  • 非法指令
  • abort()/断言失败

22. 多线程 detach 后还能 join

不能。detach 后线程与 std::thread 对象分离,不可再 join


23. Linux 内核五大模块(常见说法)

  • 进程调度
  • 内存管理
  • 文件系统
  • 网络子系统
  • 设备驱动(含中断处理)

24. 程序如何运行起来 & 执行 ls 过程

  • shell 解析命令
  • fork 创建子进程
  • 子进程 execve("/bin/ls", ...)
  • 内核加载 ELF、建立虚拟地址空间、映射动态库
  • 运行 _start -> libc 初始化 -> main
  • 输出经 write 到终端

25. 32 位系统地址空间(0-3G / 3-4G)

典型 Linux 32 位:

  • 0~3G 用户空间
  • 3~4G 内核空间

(具体划分与内核配置有关)


26. 用户态与内核态区别、如何切换

  • 用户态权限受限,不能直接执行特权指令
  • 内核态可访问硬件和内核资源
  • 切换入口:系统调用、异常、中断

27. Linux 虚拟内存如何映射物理内存

  • 每进程独立虚拟地址空间
  • 通过多级页表映射到物理页框
  • TLB 缓存热点地址翻译
  • 缺页时触发 page fault,由内核分配/换入页面

28. 进程和线程区别、使用场景

  • 进程:资源隔离强,通信成本高
  • 线程:共享地址空间,切换更轻量

场景:

  • 高隔离任务用多进程
  • 高并发共享数据用多线程

29. 僵尸进程/孤儿进程及避免方式

  • 僵尸进程:子进程退出后父进程未 wait,保留 PCB
  • 孤儿进程:父进程先退出,子进程被 init/systemd 接管

避免僵尸:

  • 父进程正确 wait/waitpid
  • 处理 SIGCHLD

30. TCP/UDP 区别、协议栈、报文从网卡到应用

  • TCP:面向连接、可靠、有序、流式
  • UDP:无连接、尽力而为、报文边界保留

数据路径(简化):

  • 网卡 DMA 到内存
  • 触发中断/NAPI 轮询
  • 进入内核协议栈(L2->IP->TCP/UDP)
  • 根据四元组投递 socket 接收队列
  • 应用 recv/read 取走数据

31. TCP 状态机、TIME_WAITCLOSE_WAIT

  • TIME_WAIT:主动关闭方在 2MSL 等待,确保最后 ACK 可重传、旧报文自然消亡
  • CLOSE_WAIT:被动关闭方收到 FIN 后,等待本地应用关闭

减少 TIME_WAIT

  • 连接复用(长连接)
  • 合理调端口范围、连接策略
  • 服务端架构上避免短连接风暴

32. TCP 粘包问题

TCP 是字节流,没有消息边界。解决:

  • 固定长度协议
  • 分隔符协议
  • 长度字段 + payload(最常见)

33. 滑动窗口满了怎么办

  • 接收方窗口为 0:发送方进入 persist 探测
  • 发送方需等待 ACK 或窗口更新
  • 可通过应用层消费速度、缓冲区参数优化

34. 网络调试常用命令

  • ss -lntp / netstat
  • tcpdump
  • ping / traceroute
  • ip addr / ip route
  • lsof -i
  • ethtool / sar -n

35. select vs epoll,LT/ET

  • select:fd 集合拷贝 + 线性扫描,fd 数受限
  • epoll:事件驱动,通常更适合高并发
  • LT(水平触发):只要有数据就持续通知
  • ET(边缘触发):状态变化通知一次,要求一次性读空

36. Socket 调优参数

  • SO_REUSEADDR / SO_REUSEPORT
  • SO_SNDBUF / SO_RCVBUF
  • TCP_NODELAY
  • SO_KEEPALIVE
  • somaxconntcp_max_syn_backlog

37. 调试:高内存 / 高 CPU / 崩溃 / 内存泄漏如何定位

  • 高 CPU:top + perf top/record/report
  • 高内存:pmapsmem、heap profiler
  • 崩溃:coredumpctl + gdb core
  • 泄漏:valgrind、ASan/LSan

方法论:先观测(指标/日志)-> 缩小范围 -> 复现 -> 证据链闭环。


38. 多线程死锁如何避免,内存踩踏如何定位

死锁避免:

  • 固定加锁顺序
  • 尽量缩小临界区
  • 使用 std::lock/层级锁
  • 设置超时与监控告警

内存踩踏定位:

  • ASan/UBSan
  • Electric Fence / guard page
  • valgrind --tool=memcheck

39. GDB、valgrind、bpftrace、perf

  • GDB:断点、回溯、查看变量/线程栈
  • valgrind:泄漏、越界、未初始化读
  • perf:CPU 火焰图与热点函数
  • bpftrace:内核/用户态动态追踪,低侵入线上排查

40. 算法题思路

40.1 二叉树

  • 最近公共祖先(LCA):递归返回左右子树命中情况
  • 对称二叉树:比较 left.left vs right.rightleft.right vs right.left

40.2 回溯

  • 全排列:used[] + path
  • 单词搜索:DFS + visited + 回溯复原

40.3 动态规划

  • 明确状态定义、转移方程、边界
  • 先小规模手推,再写代码

40.4 DFS / BFS

  • DFS 适合路径搜索、回溯
  • BFS 适合最短步数(无权图)

面试回答建议(加分项)

  1. 先给结论(30 秒)
  2. 补原理(1~2 分钟)
  3. 落场景(项目中如何用)
  4. 讲取舍(优缺点、为何选这个)
  5. 可观测性(怎么验证有效)

把“八股”答成“工程能力”的关键,是你能否说出:

  • 当时的约束条件
  • 你做的权衡
  • 上线后的数据变化

祝你面试顺利,拿到满意 offer。

本文由作者按照 CC BY 4.0 进行授权