1. 为什么这三块知识总被放在同一张面试桌上
如果你翻过几十份C/C++方向的招聘要求,会发现一个很固定的组合:语言基础、操作系统、计算机网络。很多人把这三样当成三门独立的课去背,结果面试时被追问两句就露馅。真正的问题在于,面试官考的不是你记住了多少结论,而是你能不能把这三层知识串成一条线。
举个最典型的例子。面试官问:“一个TCP连接上发送数据,从调用send到对方recv,中间发生了什么?”这个问题表面是网络题,实际上要你讲清楚用户态到内核态的切换、协议栈的缓冲区管理、网卡中断处理、进程调度唤醒。你只背TCP三次握手,这题就答不下去。
我见过太多候选人,C语言语法题做得飞快,指针、内存对齐、虚函数表都能说,但一被问到“malloc申请1字节实际占多少内存”“为什么fork之后父子的文件描述符指向同一个文件表项”就卡住。原因很简单:他们把C语言当成纯语法在学,没有和操作系统挂钩。
所以这篇面经不是给你一份背诵清单。我想做的是把这三块知识按“面试官会怎么串着问”的逻辑重新组织一遍,每一块都讲清楚底层原理、常见追问方向、以及我实际面试中踩过的坑。适合准备校招或社招C/C++岗位的人,也适合工作几年但基础有些模糊的开发者拿来查漏补缺。
2. C/C++语言层面:面试官真正想筛掉的是什么人
2.1 指针与内存模型:从“会用”到“说得清”
指针题几乎是必考,但面试官早就过了问“指针和引用区别”的阶段。现在更常见的是给一段代码让你判断行为,比如:
char *p = "hello"; char q[] = "hello";这两行的区别,能答出“p指向只读数据段,q是栈上数组可修改”只是及格。追问会接着来:sizeof(p)和sizeof(q)分别是多少?p++合法吗?q++呢?如果对p做写操作会发生什么?这一串问下来,考的是你对内存分区的理解,不是指针语法。
再往深走,面试官会问结构体内存对齐。很多人背过“按最大成员对齐”,但实际规则要复杂一些。以64位系统为例:
struct A { char a; int b; char c; };sizeof(struct A)是多少?答案是12,不是9。原因是int要求4字节对齐,a后面补3字节,c后面再补3字节保证整体是4的倍数。如果换成double成员,对齐边界变成8,结果又不一样。这个知识点面试官喜欢结合#pragma pack一起问,考察你知不知道怎么控制对齐、为什么要控制。
实操心得:回答内存对齐时,主动说出“对齐是为了让CPU一次总线周期就能读到数据,未对齐访问在某些架构上会触发异常或性能下降”,比单纯背规则加分很多。
2.2 虚函数与对象模型:别只背“多态”两个字
C++岗位必问虚函数。基础问法是“虚函数怎么实现多态”,标准答案是虚函数表加虚表指针。但面试官真正想区分水平的地方在于细节:
- 虚表指针在对象内存布局的哪个位置?构造期间虚表指针怎么变化?
- 构造函数能不能是虚函数?为什么?
- 析构函数为什么建议声明为虚函数?什么情况下不需要?
- 多重继承下虚表怎么组织?菱形继承的虚基类表又是什么?
我印象很深的一次面试,面试官让我写出下面代码的输出:
class Base { public: Base() { foo(); } virtual void foo() { cout << "Base::foo" << endl; } }; class Derived : public Base { public: void foo() override { cout << "Derived::foo" << endl; } }; Derived d;答案是Base::foo。原因是构造基类部分时,虚表指针还指向基类的虚表,派生类还没构造完,多态不会生效。这个点如果没踩过坑,很难答对。
2.3 智能指针与RAII:现代C++的分水岭
现在面试如果还只写裸指针管理资源,基本会被认为停留在旧标准。unique_ptr、shared_ptr、weak_ptr的考察点集中在:
| 智能指针 | 所有权语义 | 引用计数 | 典型追问 |
|---|---|---|---|
| unique_ptr | 独占 | 无 | 怎么转移所有权?能放进容器吗? |
| shared_ptr | 共享 | 有 | 循环引用怎么破?线程安全吗? |
| weak_ptr | 不增加计数 | 无 | 怎么提升为shared_ptr? |
shared_ptr的线程安全问题特别容易被问。控制块的引用计数是原子操作,线程安全的;但被管理对象的读写不是线程安全的。这个区分很多人答不上来。还有enable_shared_from_this的使用场景,为什么不能在构造函数里调用shared_from_this(),这些都是实战中才会遇到的坑。
2.4 我踩过的C++面试坑
有一次被问到“std::move到底做了什么”。我回答“把左值转成右值”,面试官追问“它移动了什么内存吗”。其实std::move本身不移动任何东西,它只是一个类型转换,真正的移动发生在移动构造或移动赋值里。这个理解偏差让我当时很尴尬。
还有一次问“vector扩容为什么是2倍或1.5倍”。除了摊还分析的时间复杂度,面试官还想听你讲内存碎片和缓存友好性。1.5倍相比2倍,更容易复用之前释放的内存块,减少碎片。这种细节才是拉开差距的地方。
3. 操作系统:从进程线程到内存管理的追问链
3.1 进程与线程:面试官爱问的“边界情况”
“进程和线程区别”是入门题,但追问往往从这里开始分叉。常见追问链是这样的:
- 线程共享哪些资源?独享哪些?
- 线程切换和进程切换开销差在哪?
- 协程和线程的区别?为什么协程切换更轻?
- 一个进程最多能创建多少线程?受什么限制?
第4个问题很能看出功底。答案不是固定的,受虚拟内存空间、线程栈大小、系统参数限制。32位系统用户态地址空间约3GB,默认线程栈8MB,理论上限约380个左右,但实际远小于这个数,因为还有堆、代码段等占用。64位系统地址空间大得多,限制主要来自物理内存和内核参数。
3.2 内存管理:虚拟内存不是“虚拟的”
虚拟内存是操作系统面试的核心。面试官喜欢从malloc一路问到页表:
malloc(1)和malloc(4096)在底层行为上有什么区别?- 什么是缺页中断?发生缺页中断时操作系统做了什么?
- 页表多大?多级页表怎么节省空间?
- TLB是什么?为什么它能加速地址转换?
malloc小内存时从堆的空闲链表分配,大内存(通常超过128KB)会直接用mmap映射一块匿名区域。这个阈值可以通过mallopt调整。缺页中断的处理流程是:CPU访问虚拟地址,MMU查页表发现页不在内存,触发中断,内核分配物理页、从磁盘读入、更新页表、返回用户态重新执行指令。
多级页表的设计逻辑值得展开。32位系统二级页表,顶级页表1024项,每项指向一个二级页表,二级页表1024项,每项映射4KB页。这样只有实际使用的地址区间才需要二级页表,节省大量空间。64位系统通常用四级或五级页表。
3.3 进程间通信与同步:别只会说“管道、消息队列、共享内存”
IPC的考察重点不是列举,而是选型理由和底层实现。比如:
- 管道为什么是半双工的?匿名管道和命名管道区别在哪?
- 共享内存为什么是最快的IPC?它的同步问题怎么解决?
- 信号和信号量的区别?信号处理函数有什么限制?
共享内存快是因为数据不需要在用户态和内核态之间拷贝,直接映射同一块物理内存。但多个进程同时读写需要额外同步,通常配合信号量或互斥锁使用。信号处理函数的限制很多人不知道:它必须是可重入的,不能调用非异步信号安全的函数,比如printf、malloc都不安全。
3.4 调度与锁:从自旋锁到无锁队列
锁的问题是C++后端面试的高频区。面试官会从“互斥锁和自旋锁区别”开始,一路问到“什么是ABA问题”“内存序有哪几种”。
自旋锁适合临界区极短的场景,避免线程切换开销;互斥锁适合临界区较长或可能睡眠的场景。自旋锁在单核上没意义,因为自旋的线程占着CPU,持有锁的线程没机会运行。
无锁编程的追问更深。CAS操作的ABA问题是指:线程1读到值A,准备CAS改成C;期间线程2把A改成B又改回A;线程1的CAS成功,但中间状态被忽略了。解决方案是加版本号或使用带标记的指针。内存序方面,C++11提供了六种memory_order,从relaxed到seq_cst,理解它们需要清楚编译器和CPU的重排序规则。
4. 计算机网络:从三次握手到协议栈全链路
4.1 TCP连接管理:握手挥手背后的状态机
三次握手和四次挥手是必背,但面试官现在更爱问“为什么”:
- 为什么是三次握手而不是两次?两次会有什么问题?
- 为什么挥手是四次?TIME_WAIT状态的作用是什么?持续多久?
- 如果客户端最后不发送ACK会怎样?
两次握手的问题在于:服务端无法确认客户端的接收能力。如果客户端的历史连接请求延迟到达,服务端会误以为新连接建立,浪费资源。TIME_WAIT持续2MSL(报文最大生存时间),作用是确保最后的ACK能到达服务端,以及让本次连接的旧报文在网络中消散,避免影响新连接。
状态转换图要能画出来。CLOSE_WAIT状态过多通常意味着应用层没有正确关闭连接,TIME_WAIT过多则可能是短连接频繁创建导致。这些实战排查经验面试官很看重。
4.2 TCP可靠传输:滑动窗口、拥塞控制、重传
可靠传输的机制要能串起来讲:
- 滑动窗口怎么实现流量控制?
- 拥塞控制有哪几个阶段?慢启动、拥塞避免、快重传、快恢复分别做什么?
- 超时重传的时间怎么计算?
滑动窗口的本质是接收方告诉发送方“我还能收多少”,发送方根据这个调整发送速率。拥塞控制则是发送方根据网络状况自我调节。慢启动阶段窗口指数增长,到达阈值后进入拥塞避免线性增长,检测到丢包时根据情况进入快重传或超时重传。
RTO的计算用Jacobson算法,综合平滑RTT和RTT偏差。这个算法保证了重传时间既不过于激进也不过于保守。
4.3 从输入URL到页面展示:全链路串讲
这是网络面试的经典大题,也是检验你能否把各层知识串起来的最好方式。完整链路包括:
- DNS解析:浏览器缓存、系统缓存、递归查询
- TCP连接:三次握手
- TLS握手(如果是HTTPS):证书验证、密钥交换
- HTTP请求:请求行、请求头、请求体
- 服务端处理:负载均衡、反向代理、应用逻辑
- HTTP响应:状态码、响应头、响应体
- 浏览器渲染:HTML解析、CSSOM、渲染树、布局、绘制
每一层都能展开追问。比如DNS用UDP还是TCP?为什么?TLS握手用了几次RTT?TLS1.3怎么优化到1-RTT甚至0-RTT?HTTP/2的多路复用怎么解决队头阻塞?这些问题层层递进,能答到哪一层直接反映你的知识深度。
4.4 网络编程实战:epoll与IO多路复用
C++后端岗位几乎必问epoll。面试官会从“select、poll、epoll区别”开始,深入到:
- epoll的水平触发和边缘触发区别?边缘触发为什么必须配非阻塞IO?
- epoll的红黑树和就绪链表怎么工作?
- 惊群问题是什么?怎么解决?
边缘触发下,如果一次没读完数据,后续不会再次通知,所以必须循环读到EAGAIN。非阻塞IO保证读操作不会阻塞住整个线程。epoll内部用红黑树管理监听的fd,用就绪链表存放已触发的事件,epoll_wait直接检查就绪链表,不需要遍历所有fd,这是它比select高效的根本原因。
惊群问题在多个进程或线程同时等待同一个epoll实例时出现。解决方案包括使用EPOLLEXCLUSIVE标志、或者只让一个线程负责accept。
5. 把三块知识串起来的面试实战策略
5.1 面试官怎么设计连环追问
面试官的追问不是随机的,通常沿着“语言→系统→网络”或者反向的链路走。比如从shared_ptr的线程安全问到原子操作的实现,再问到CPU缓存一致性协议,最后问到网络编程中多线程模型的选型。这条链路考的是你能否把不同层次的知识关联起来。
我总结了几条常见的追问链路:
| 起点 | 中间追问 | 终点 |
|---|---|---|
| malloc | 虚拟内存、缺页中断 | 页表、TLB |
| 虚函数 | 对象内存布局 | 构造析构顺序 |
| TCP握手 | 状态机、TIME_WAIT | 端口耗尽排查 |
| epoll | 红黑树、就绪链表 | 多线程Reactor模型 |
| 智能指针 | 引用计数原子性 | 内存序、无锁编程 |
准备的时候按这些链路去串,比孤立地背知识点有效得多。
5.2 回答节奏与深度控制
面试中的回答节奏很关键。我的经验是:先给结论,再展开一层原理,然后主动留一个“钩子”让面试官追问。比如被问到“进程和线程区别”,先答“线程是调度的基本单位,进程是资源分配的基本单位”,然后补一句“线程切换不需要切换页表,所以开销更小”,面试官很可能顺着问“那协程呢”。这样你就把节奏握在自己手里。
但要注意别过度展开。如果面试官只是随口一问,你滔滔不绝讲五分钟,反而显得不会抓重点。观察面试官的反应,如果他继续追问细节,就深入;如果他点头转向下一个问题,就收住。
5.3 我实际面试中遇到的“送命题”
有一次面试官问:“你刚才说epoll边缘触发必须配非阻塞IO,那如果我就是用阻塞IO会怎样?”这个问题很刁钻。阻塞IO下,边缘触发通知后你循环读,最后一次读会阻塞住,因为没数据了但fd是阻塞的。整个线程卡死,其他连接得不到处理。这个场景如果你没实际写过代码,很难答得具体。
还有一次问:“shared_ptr的引用计数是原子的,那它线程安全吗?”标准答案是:引用计数的增减是线程安全的,但被管理对象的读写不是。如果两个线程同时读同一个shared_ptr指向的对象,没问题;如果一个读一个写,需要额外同步。这个区分我见过至少一半候选人答错。
5.4 复习路径与资料选择
最后说下复习路径。我的建议是按“链路”复习,不要按“科目”复习。具体做法:
- 拿一张白纸,画出从C代码到网络包的完整路径
- 每个节点问自己三个问题:是什么、为什么这样设计、有什么替代方案
- 找几个开源项目(比如简单的HTTP服务器)读源码,看这些知识怎么落地
- 模拟面试,找人互相追问,训练表达节奏
资料方面,经典书要读,但别只读书。操作系统可以看《操作系统导论》,网络可以看《TCP/IP详解》,C++可以看《深度探索C++对象模型》。但更重要的是动手写代码验证,比如自己实现一个简单的线程池、一个基于epoll的服务器,写的过程中会遇到各种书上没讲的问题,这些才是面试时能讲出彩的素材。
注意:面试中遇到不会的问题,不要硬编。诚实说“这块我了解不深,但我的理解是……”,然后给出你的推理过程。面试官更看重思考方式,而不是标准答案。