C/C++ 后端高频面试题系统梳理(编译、内存、并发、Linux、网络、调试、算法)
C/C++ 后端高频面试题系统梳理(编译、内存、并发、Linux、网络、调试、算法)
前言
这篇文章把 C/C++ 后端面试中常见的“原理 + 场景”题做一次体系化整理,重点是:
- 能解释底层机制(不是只背结论)
- 能结合项目场景(为什么这么做、带来什么收益)
- 能延展到排障与优化(线上问题怎么定位)
1. GCC 编译四个阶段分别做了什么
典型流程:预处理 -> 编译 -> 汇编 -> 链接
- 预处理(cpp)
- 处理
#include、#define、条件编译 - 删除注释,展开宏
- 输出
.i
- 处理
- 编译(cc1/cc1plus)
- 词法、语法、语义分析
- 优化并生成汇编代码
- 输出
.s
- 汇编(as)
- 将汇编指令翻译为机器码
- 输出目标文件
.o
- 链接(ld)
- 解析符号引用、合并段、重定位
- 静态链接库/动态链接库处理
- 生成可执行文件或共享库
常用查看:
gcc -E a.c -o a.igcc -S a.c -o a.sgcc -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. malloc 与 new 区别,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_map、vector 扩容、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_relaxedacquire/releaseacq_relseq_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_WAIT 和 CLOSE_WAIT
TIME_WAIT:主动关闭方在 2MSL 等待,确保最后 ACK 可重传、旧报文自然消亡CLOSE_WAIT:被动关闭方收到 FIN 后,等待本地应用关闭
减少 TIME_WAIT:
- 连接复用(长连接)
- 合理调端口范围、连接策略
- 服务端架构上避免短连接风暴
32. TCP 粘包问题
TCP 是字节流,没有消息边界。解决:
- 固定长度协议
- 分隔符协议
- 长度字段 + payload(最常见)
33. 滑动窗口满了怎么办
- 接收方窗口为 0:发送方进入 persist 探测
- 发送方需等待 ACK 或窗口更新
- 可通过应用层消费速度、缓冲区参数优化
34. 网络调试常用命令
ss -lntp/netstattcpdumpping/tracerouteip addr/ip routelsof -iethtool/sar -n
35. select vs epoll,LT/ET
select:fd 集合拷贝 + 线性扫描,fd 数受限epoll:事件驱动,通常更适合高并发- LT(水平触发):只要有数据就持续通知
- ET(边缘触发):状态变化通知一次,要求一次性读空
36. Socket 调优参数
SO_REUSEADDR/SO_REUSEPORTSO_SNDBUF/SO_RCVBUFTCP_NODELAYSO_KEEPALIVEsomaxconn、tcp_max_syn_backlog
37. 调试:高内存 / 高 CPU / 崩溃 / 内存泄漏如何定位
- 高 CPU:
top+perf top/record/report - 高内存:
pmap、smem、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.leftvsright.right、left.rightvsright.left
40.2 回溯
- 全排列:
used[] + path - 单词搜索:DFS + visited + 回溯复原
40.3 动态规划
- 明确状态定义、转移方程、边界
- 先小规模手推,再写代码
40.4 DFS / BFS
- DFS 适合路径搜索、回溯
- BFS 适合最短步数(无权图)
面试回答建议(加分项)
- 先给结论(30 秒)
- 补原理(1~2 分钟)
- 落场景(项目中如何用)
- 讲取舍(优缺点、为何选这个)
- 可观测性(怎么验证有效)
把“八股”答成“工程能力”的关键,是你能否说出:
- 当时的约束条件
- 你做的权衡
- 上线后的数据变化
祝你面试顺利,拿到满意 offer。
本文由作者按照 CC BY 4.0 进行授权