ACM 竞赛背景去面后端/研发岗,这三块是必考的。整理成问答形式,方便快速过一遍。
一、计算机网络
TCP vs UDP
| TCP | UDP | |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认、重传、排序) | 不可靠 |
| 速度 | 慢(有开销) | 快 |
| 适合场景 | HTTP、文件传输、数据库 | 视频流、DNS、游戏 |
TCP 三次握手 / 四次挥手
三次握手(建立连接):
1 | 客户端 → SYN(seq=x) → 服务端 [客户端: SYN_SENT] |
为什么是三次不是两次?两次握手服务端无法确认客户端能收到自己的消息,可能建立无效连接。
四次挥手(断开连接):
1 | 客户端 → FIN → 服务端 [客户端: FIN_WAIT_1] |
为什么四次?TCP 是全双工,两个方向需要分别关闭。服务端收到 FIN 后可能还有数据要发,所以 ACK 和 FIN 分开发。
TIME_WAIT 为什么等 2MSL?
确保最后一个 ACK 能到达服务端(如果丢了,服务端会重发 FIN,客户端需要能响应)。
HTTP/1.1 vs HTTP/2 vs HTTP/3
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC(基于 UDP) |
| 多路复用 | 不支持(队头阻塞) | 支持(同一连接多流) | 支持(流级别独立) |
| 头部压缩 | 无 | HPACK | QPACK |
| 队头阻塞 | 有 | TCP 层仍有 | 彻底解决 |
HTTP/2 多路复用原理: 把请求拆成帧(frame),多个请求的帧交错发送,接收端按 stream ID 重组。一个连接并发多个请求,不需要多开 TCP 连接。
HTTPS 握手过程
1 | 1. 客户端 → ClientHello(支持的加密套件、随机数 C) |
TLS 1.3 简化为 1-RTT(甚至 0-RTT 恢复连接)。
DNS 解析过程
1 | 浏览器缓存 → 系统 hosts → 本地 DNS 服务器 |
TCP 拥塞控制
四个阶段:
- 慢启动:cwnd 从 1 开始,每个 RTT 翻倍(指数增长)
- 拥塞避免:cwnd 超过阈值(ssthresh)后,每 RTT 加 1(线性增长)
- 快重传:收到 3 个重复 ACK,立即重传,不等超时
- 快恢复:快重传后 ssthresh = cwnd/2,cwnd = ssthresh,进入拥塞避免
常见状态码
| 码 | 含义 |
|---|---|
| 200 | OK |
| 301 | 永久重定向 |
| 302 | 临时重定向 |
| 304 | Not Modified(缓存有效) |
| 400 | Bad Request(客户端参数错误) |
| 401 | Unauthorized(未认证) |
| 403 | Forbidden(无权限) |
| 404 | Not Found |
| 429 | Too Many Requests(限流) |
| 500 | Internal Server Error |
| 502 | Bad Gateway(上游服务挂了) |
| 503 | Service Unavailable |
二、操作系统
进程 vs 线程 vs 协程
| 进程 | 线程 | 协程 | |
|---|---|---|---|
| 资源 | 独立地址空间 | 共享进程资源 | 共享线程资源 |
| 切换开销 | 大(上下文切换) | 中 | 极小(用户态切换) |
| 通信 | IPC(管道、共享内存) | 共享内存(需加锁) | 直接共享(单线程内) |
| 崩溃影响 | 不影响其他进程 | 可能导致整个进程崩溃 | 影响当前线程 |
协程的本质: 用户态的”假线程”,由程序自己调度(不是 OS),切换时只保存少量寄存器,开销极小。Python 的 async/await、Go 的 goroutine 都是协程思想。
死锁
四个必要条件(Coffman 条件):
- 互斥:资源同时只能被一个进程持有
- 持有并等待:进程持有资源同时等待其他资源
- 不可剥夺:资源不能被强制取走
- 循环等待:进程间形成等待环
预防死锁: 破坏任意一个条件。最常用:资源有序分配(破坏循环等待)、一次性申请所有资源(破坏持有并等待)。
内存管理
虚拟内存: 每个进程有独立的虚拟地址空间,通过页表映射到物理内存。好处:进程隔离、内存可以超过物理内存(换页到磁盘)。
页面置换算法:
- LRU(最近最少使用):淘汰最久没被访问的页,实际用近似算法(时钟算法)
- FIFO:淘汰最早进入的页,可能出现 Belady 异常
- OPT(最优):淘汰未来最久不用的页,理论最优但不可实现
内存碎片:
- 内部碎片:分配的内存比实际需要大(固定分区)
- 外部碎片:空闲内存总量够但不连续(动态分区)
- 解决:分页(消除外部碎片)、内存紧缩
进程调度算法
| 算法 | 特点 | 适合场景 |
|---|---|---|
| FCFS | 先来先服务,简单但可能饥饿 | 批处理 |
| SJF | 最短作业优先,平均等待时间最短 | 批处理 |
| 时间片轮转 | 公平,响应时间有保证 | 交互式系统 |
| 优先级调度 | 高优先级先执行,可能饥饿 | 实时系统 |
| 多级反馈队列 | 综合以上,Linux 实际使用 | 通用 |
锁
互斥锁(Mutex): 同一时刻只有一个线程持有,其他线程阻塞等待(睡眠)。
自旋锁(Spinlock): 等待时不睡眠,循环检查(忙等)。适合锁持有时间极短的场景,避免线程切换开销。
读写锁: 多个读者可以并发,写者独占。适合读多写少场景。
乐观锁 vs 悲观锁:
- 悲观锁:先加锁再操作(Mutex)
- 乐观锁:不加锁,提交时检查是否有冲突(CAS、数据库版本号)
系统调用
用户态程序不能直接访问硬件,需要通过系统调用陷入内核态。过程:
1 | 用户程序调用 read() → 触发软中断(int 0x80 / syscall 指令) |
常见系统调用:fork、exec、open/read/write、mmap、socket。
三、C++ 语法
指针 vs 引用
| 指针 | 引用 | |
|---|---|---|
| 可为空 | 可以(nullptr) |
不可以 |
| 可重新绑定 | 可以 | 不可以(初始化后固定) |
| 有自己的地址 | 有 | 没有(是别名) |
| 算术运算 | 支持 | 不支持 |
引用本质是编译器保证非空的指针别名,通常用于函数参数传递避免拷贝。
智能指针
C++11 引入,解决手动 delete 导致的内存泄漏和悬空指针问题。
1 | // unique_ptr:独占所有权,不可复制,可移动 |
循环引用问题: A 持有 B 的 shared_ptr,B 持有 A 的 shared_ptr,两者引用计数永远不为 0,内存泄漏。解决:其中一方改用 weak_ptr。
移动语义 / 右值引用
1 | // 左值:有名字,有地址 |
移动语义的意义:避免深拷贝,把 O(n) 的拷贝变成 O(1) 的指针转移。
虚函数 / 多态
1 | class Animal { |
虚函数表(vtable): 每个有虚函数的类有一个 vtable,存放虚函数指针。对象内存里有一个 vptr 指向 vtable。调用虚函数时通过 vptr 查表,有一次间接寻址开销。
纯虚函数 / 抽象类:
1 | class Shape { |
内存布局
1 | 栈(Stack):局部变量、函数参数,自动管理,LIFO,空间小(通常 8MB) |
常见陷阱
1. 未初始化变量
1 | int x; |
2. 数组越界
1 | int arr[5]; |
3. 悬空指针
1 | int* p = new int(42); |
4. 对象切片
1 | Dog dog; |
STL 容器选型
| 容器 | 底层 | 查找 | 插入/删除 | 适合场景 |
|---|---|---|---|---|
vector |
动态数组 | O(n) | 尾部 O(1),中间 O(n) | 随机访问,尾部增删 |
deque |
分段数组 | O(1) | 两端 O(1) | 双端队列 |
list |
双向链表 | O(n) | 任意位置 O(1) | 频繁中间插删 |
map |
红黑树 | O(log n) | O(log n) | 有序键值对 |
unordered_map |
哈希表 | O(1) 均摊 | O(1) 均摊 | 无序键值对,高频查找 |
set |
红黑树 | O(log n) | O(log n) | 有序不重复集合 |
priority_queue |
堆 | O(1) 取最值 | O(log n) | 堆/优先队列 |
Lambda 表达式
1 | // [捕获列表](参数) -> 返回类型 { 函数体 } |
模板
1 | // 函数模板 |
模板在编译期展开,零运行时开销,但会增加编译时间和二进制体积。
面试高频问题汇总
Q:TCP 为什么可靠?
序列号保证有序、确认号 + 超时重传保证不丢、滑动窗口控制流量、拥塞控制避免网络拥塞。
Q:进程间通信方式?
管道(pipe)、消息队列、共享内存(最快)、信号量(同步)、Socket(跨机器)。
Q:malloc 和 new 的区别?malloc 是 C 库函数,只分配内存;new 是 C++ 运算符,分配内存 + 调用构造函数。free vs delete 同理。new[] 对应 delete[],不能混用。
Q:const 的用法?
1 | const int* p; // 指向常量的指针,不能通过 p 修改值 |
Q:static 的用法?
- 局部变量:生命周期延长到程序结束,只初始化一次
- 成员变量/函数:属于类而非对象,所有实例共享
- 全局变量/函数:限制作用域在当前文件(内部链接)
Q:sizeof 结构体为什么不等于成员之和?
内存对齐。编译器为了 CPU 访问效率,会在成员之间插入 padding,使每个成员的地址是其大小的整数倍。可以用 #pragma pack(1) 关闭对齐(但可能降低性能)。