C++ 后端开发面试题整理
最近把 C++ / Linux 后端方向面试里被反复问到的问题按模块整理了一遍,去掉了一些零散的口水话,留下"能直接复习"的部分。内容偏校招 / 实习难度,覆盖 C++ 语言、内存管理、STL、编译工具链、操作系统、计算机网络、数据库和 Redis,最后附一份高频常数速查表和几条比知识点更容易踩的坑。
适合谁看:准备 C++ 后端方向的校招 / 实习,已经会写代码但"名词题第一句话说不准""一被追问第二层就断"的同学。
食用方式 :每个题目只用一句话给出核心定义,后面才是展开,建议先盖住答案自己说一遍。面试里真正决定分数的不是"你知不知道",而是"你能不能在三秒内把第一句说准"。
目录
- [一、C++ 语言基础](#一、C++ 语言基础)
- 二、内存管理
- 三、STL
- 四、编译与调试工具链
- 五、操作系统
- 六、计算机网络
- 七、数据库
- 八、Redis
- 九、高频常数速查表
- 十、面试经验:几条比知识点更容易踩的坑
一、C++ 语言基础
1.1 什么是函数重载?
一句话 :同一作用域内、函数名相同、参数列表不同 的多个函数,与返回类型无关。
- 参数列表不同体现在类型 / 个数 / 顺序,任何一项不同就构成重载。
- 编译期由名称修饰(name mangling)生成不同符号完成决议,属于静态多态。
- C 语言不支持重载,因为 C 的符号不做名字修饰。
- 常见误区:只改返回类型不构成重载;
const成员函数与非const版本可以重载(因为隐含的this类型不同)。
⚠️ 别把"编译期决议"说成"预编译阶段"。预处理阶段只做文本替换 ,重载决议发生在编译期。
1.2 指针和引用的区别
| 维度 | 指针 | 引用 |
|---|---|---|
| 本质 | 一个对象,存地址,x64 下占 8 字节 | 不是独立对象,是别名;但底层实现通常就是一个指针 |
| 能否为空 | 可以 nullptr |
不能为空,必须初始化且绑定后不能改绑 |
| 多级 | 可以 int** |
没有多级引用(引用折叠得到的是左值/右值引用,跟"多级"不是一回事) |
sizeof |
8(x64) | 被引用对象的大小 |
&r |
得到指针自身的地址 | 得到被引用对象的地址 |
++ |
移动到下一个元素 | 语法上合法,但作用在被引用对象上,没有"指向下一个"的语义 |
两个高频陷阱:
- 说"引用不占空间"是不严谨 的,正确的说法是"引用在语言层面不是独立对象"。
- "引用的引用 = 将亡值 / 右值引用"是错的 。标准里"引用的引用"通过引用折叠得到左值引用或右值引用,和"将亡值"是两个概念。
1.3 #define 和 typedef 的区别
一句话 :#define 是预处理指令 ,做纯文本替换 ;typedef 是编译期 的类型别名,参与语法和类型检查。
| 维度 | #define |
typedef |
|---|---|---|
| 处理阶段 | 预处理期,文本替换 | 编译期,编译器理解 |
| 类型检查 | 无 | 有语法 / 语义检查 |
| 作用域 | 无(文件级,直到 #undef) |
遵循 C++ 作用域 |
| 调试信息 | 看不到宏名 | 调试器里有别名 |
| 经典陷阱 | #define PINT int* → PINT a,b; 只有 a 是指针 |
typedef int* PINT; → a、b 都是指针 |
#define 的三类用途与各自的坑:
- 常量宏
#define MAX_SIZE 1024:无类型、调试器看不到符号、报错报的是字面量。现在推荐constexpr/const。 - 带参宏
#define MAX(a,b) ((a)>(b)?(a):(b)):参数和整体都必须加括号 ,否则MAX(a+1,b)展开后优先级出错;而且参数会被求值两次 ,传MAX(i++, j)会 double increment。 - 条件编译
#ifdef DEBUG、头文件卫士#ifndef XXX_H。现在推荐#pragma once。
收口一句 :能用 typedef / using / constexpr / inline 表达的,就不要用 #define。
1.4 多态:静态多态与动态多态
一句话 :多态就是同一个接口作用于不同类型的对象时表现出不同的行为 ,分编译期决议 和运行期决议两类。
① 静态多态(编译期决议)
- 函数重载:同名不同参,编译器靠 name mangling 在编译期绑定。
- 模板(泛型) :
template<typename T>按实参类型实例化出不同的函数 / 类。 - CRTP(奇异递归模板):编译期的"静态虚函数"。
- 另外派生类同名函数会**隐藏(name hiding)**基类版本,也是编译期绑定,但这是"隐藏"而不是"重载"。
② 动态多态(运行期决议)
- 虚函数 ------ 这是语言层面唯一的运行期多态机制。
- 实现原理:对象内存头部有 **vptr(虚表指针)**指向该类的 vtable(虚函数表) ;通过基类指针 / 引用调用虚函数时,走"取 vptr → 查 vtable 拿真实地址 → 调用",所以叫动态绑定。
- 严格补充:函数指针、
std::function、策略模式、RTTI(typeid/dynamic_cast)也能实现运行期分发,但它们属于设计层面的手段,不是语言定义的动态多态。
⚠️ 高频口误:把"动态多态"说成"动态重载"。重载没有动态这一说;重载属静态,虚函数属动态,一旦把维度混了,面试官会直接判定基础不牢。
1.5 虚函数与虚函数表
一句话 :虚函数是用 virtual 修饰的成员函数,运行期通过 vptr → vtable → 真实函数地址实现动态绑定。
关于 vtable 的几个必答点:
- vtable 存在哪里? 由编译器在编译期生成 ,存放在只读数据段(
.rodata) ,每个类一份。 - 对象里存的是什么? 存的是 vptr ,位于对象内存最开头(x64 8 字节,x86 4 字节)。
- 两个同类对象共享一份虚表吗? 共享。它们的 vptr 指向同一份 vtable。
- 多继承 时对象里会有多个 vptr,各自对应一条继承链。
- 有虚函数的类,对象至少多占 8 字节(64 位下一个 vptr)。
几个"为什么":
- 构造函数为什么不能是虚函数? 调用构造函数时对象内存刚分配、vptr 尚未建立,无法查表;而且"虚函数"的前提是"已经有一个对象"。
- 静态成员函数为什么不能是虚函数? 没有
this,无法通过 vptr 解析。 - 构造 / 析构期间调用虚函数不生效:构造时 vptr 先指向基类,析构时又回退到基类。
1.6 析构函数为什么要声明为 virtual
这是最高频的连环追问,建议按"场景 → 后果 → 反向结论 → 配套"四步答。
触发场景:
cpp
Base* p = new Derived();
delete p;
Base::~Base()非虚 →delete p是静态绑定 ,只走Base::~Base()。Derived里新增的资源(自己new的数组、持有的 fd / 锁、打开的文件)永远不会被释放 ,造成泄漏;C++ 标准里这属于未定义行为(UB)。Base::~Base()为虚 → 析构变成动态绑定 :先执行Derived::~Derived(),再由编译器自动调用Base::~Base(),逆序完整析构。
规则 :只要这个类会被当作基类多态使用 (有虚函数 / 会被通过基类指针 delete),析构必须 是 virtual。
反向结论(最能加分) :不打算被继承的类不要给析构加 virtual ------ 加了会引入 vptr(对象变大 8 字节)、失去 trivially destructible 属性、影响移动语义和 constexpr 可用性。另一种做法是把基类析构声明为 protected 且非虚------禁止通过基类指针 delete,同样安全。
配套追问 :unique_ptr<Base>(new Derived) 一样要求虚析构;换成 make_unique<Derived>() 直接构造就不受基类析构非虚的影响。所以"用 make_unique / make_shared"本身也是一种防御。
1.7 纯虚函数与抽象类
virtual void f() = 0;是纯虚函数 ;含纯虚函数的类是抽象类。- 抽象类为什么不能实例化?
- 该类的 vtable 里对应槽位是未绑定的 (指向
__cxa_pure_virtual之类的占位),一旦允许创建对象,通过基类指针调用它就必然崩。 - 语义上抽象类表达的是不完整的接口契约 ,标准直接禁止:
error: cannot declare variable to be of abstract type。
- 该类的 vtable 里对应槽位是未绑定的 (指向
- 补充:抽象类可以定义指针和引用 ;纯虚函数可以给定义 (派生类可显式
Base::f()调用);派生类必须实现全部纯虚函数才能实例化,否则自己也是抽象类。
1.8 虚继承解决什么问题
一句话 :解决菱形继承。
cpp
class A {};
class B1 : public A {};
class B2 : public A {};
class D : public B1, public B2 {}; // D 里有两份 A 的子对象
问题:数据冗余 (两份 A 的成员)+ 访问二义性 (d.x 不知道指哪一份)。
解法:class B1 : virtual public A。虚继承后虚基类子对象在整个继承链里只保留一份 ,由最派生类负责构造,中间层的构造调用被忽略。
代价:引入虚基类表 / 偏移,访问虚基类成员要绕一层,对象变大、效率略降。
1.9 空类大小与对象内存布局
空类(无成员变量、无成员函数)的大小是 1 字节。 原因是标准要求"同类不同对象的地址必须不同",所以最小要占 1 字节。
延伸:
- 如果这个空类有虚函数 ,则是 8 字节(一个 vptr)。
- EBO(空基类优化):派生类可以不为空基类额外占空间。
- C++20 的
[[no_unique_address]]用于成员,可以达到类似效果。
1.10 inline 和宏的区别
| 维度 | #define 宏 |
inline 函数 |
|---|---|---|
| 阶段 | 预处理期,纯文本替换 | 编译期,编译器理解 |
| 类型 / 语法检查 | 无 | 有 |
| 作用域 | 无 | 遵循 C++ 作用域 |
| 调试 | 看不到宏名 | 有符号信息 |
| 陷阱 | 参数要加括号、参数被求值两次 | 无 |
调用上的区别 :inline 函数会在调用点直接展开 ,省掉普通函数的压栈 / 跳转 / 返回 / 参数拷贝 开销;代价是代码膨胀(icache 压力)。
要点:
inline只是给编译器的建议,不保证一定展开。递归、取地址、虚函数调用一般不会内联。- 类内定义的成员函数默认是
inline。 - 现代替代方案:常量宏 →
constexpr/const;函数宏 →inline函数 / 模板;头文件卫士 →#pragma once。
1.11 Lambda 表达式
本质 :编译器生成一个匿名的仿函数类(闭包类型) ,lambda 体的内容放进 operator(),捕获列表变成类的成员。
捕获列表的完整清单:
| 写法 | 含义 |
|---|---|
[=] |
值捕获,捕获的是副本 ,默认 const,要改需加 mutable |
[&] |
引用捕获,改的是本体 ,注意悬垂引用 |
[this] |
捕获 this 指针;C++17 起可 [*this] 值捕获整个对象 |
[x, &y] |
混合捕获 |
[=, &y] |
默认值捕获,y 例外用引用 |
[p = std::move(ptr)] |
C++14 起的初始化捕获,用于移动进闭包 |
最容易答错的三个点:
- 全局变量和静态变量不需要也不能被捕获 ------ 它们在静态存储区,任何地方都能直接访问。lambda 只捕获自动存储期变量 (局部变量、函数参数)。所以"lambda 能捕获全局变量吗"的正确答案是:能访问,但不叫捕获。
- 捕获的局部变量是副本。想改本体必须用引用捕获。
- 引用捕获要小心悬垂 ------ lambda 可能活得比被捕获的变量还久(比如被存进容器或异步执行)。
和仿函数的区别 :lambda 是语法糖 ,写起来短、能就地捕获上下文;仿函数是显式定义的类,可以重载多个 operator()、可以有模板参数、可以被继承。功能上等价。
其他要点:无捕获的 lambda 可以转成函数指针(C++20 起还可默认构造);泛型 lambda(auto 参数)从 C++14 开始支持。
二、内存管理
2.1 malloc / free 与 new / delete
| 维度 | malloc / free |
new / delete |
|---|---|---|
| 本质 | 库函数 (<stdlib.h> / <malloc.h>) |
运算符(语言内置,可重载) |
| 大小 | 手动算字节数 | 自动按类型算,还有 new[] |
| 初始化 | 不初始化 | 调用构造函数 |
| 失败 | 返回 NULL |
抛 std::bad_alloc(new(std::nothrow) 才返回空) |
| 释放 | free |
delete(数组用 delete[]) |
两个要修正的常见说法:
- "它们底层都用 brk / mmap" ------ 严格说 brk / mmap 是 glibc 的 ptmalloc(
malloc的实现) 用的(小块走 brk / 堆区,大块走 mmap);operator new默认也是转调malloc。 new配free、malloc配delete是 UB,必须成对使用。
2.2 三种申请内存的方式
new T/new T(args)------ 默认走全局operator new,底层是malloc。- 定位 new(placement new) :
new (ptr) T(args)------ 在已有内存 上构造对象,不申请内存。典型用途是内存池、容器预留空间后的就地构造。 - 自己重载
operator new(size_t)或使用std::allocator<T>::allocate------ 可用于内存池、自定义对齐。
另外还有 new T[n](数组 new)和 ::operator new(全局版本,用于区分类内重载)。
2.3 深拷贝与浅拷贝
- 浅拷贝 (编译器默认生成):逐成员复制。有指针成员时只复制地址 ,两个对象共享 同一块堆资源 → 析构时重复释放(double free)。
- 深拷贝 :指针成员指向的资源也重新申请一块并复制内容 → 两个对象互不影响。
结论 :有裸指针 / 持有资源的类必须自己写拷贝行为(三/五法则 :拷贝构造、拷贝赋值、析构;C++11 再加移动构造、移动赋值)。更好的做法是 Rule of Zero ------ 用 std::string、vector、智能指针管理资源,一个都不用写。
顺带把智能指针也背一下:
| 智能指针 | 语义 | 要点 |
|---|---|---|
unique_ptr |
独占所有权 | 不可拷贝,只能移动;零开销(默认删除器) |
shared_ptr |
共享所有权 | 引用计数(控制块),计数归零才析构 |
weak_ptr |
弱引用 | 不增加计数,用 lock() 提升;用来打破循环引用 |
2.4 内存泄漏
定义 :程序申请了堆内存,但丢失了对它的引用(或忘了释放),导致这块内存在进程运行期间无法再被使用、也无法归还。
一个很好用的比喻:本来有 4 个公共厕所,施工时把第 1 个堵住了,别人以为只有 3 个,第 1 个就永远没人能用------这就是泄漏。
两个容易被追问的层面(一定要分清):
- 进程运行期间 :堆内存由程序员(或分配器)管理,OS 不会主动回收。泄漏会持续累积,最终 OOM 或被 OOM Killer 杀掉。这就是"系统不管"的意思。
- 进程终止之后 :OS 会回收该进程的全部资源(地址空间、页表、打开的 fd),因为进程的虚拟地址空间本身就被销毁了。
所以完整说法是:"泄漏指的是进程生命周期内的浪费;进程一结束,泄漏的内存自然也被 OS 收回了。但长生命周期的服务不能靠重启解决,泄漏会拖垮机器。"
排查工具 :valgrind --leak-check=full、ASan(-fsanitize=address)、mtrace、观察 /proc/<pid>/status 里 VmRSS 的趋势。
三、STL
3.1 STL 六大组件
一句话 :STL = Standard Template Library,标准模板库,是 C++ 标准库中基于模板 实现的泛型编程 基础设施------核心思想是把数据结构和算法解耦,靠迭代器把两者粘起来。
六个组件(记这六个数):
- 容器 containers ------
vector/list/deque(序列),map/set(红黑树,有序),unordered_map/unordered_set(哈希,无序) - 算法 algorithms ------
sort/find/transform/lower_bound,全是模板函数,只依赖迭代器,不依赖具体容器 - 迭代器 iterators ------ 容器与算法之间的粘合层;分五类:输入、输出、前向、双向、随机访问
- 仿函数 functors ------
std::less<T>、std::greater<T>、lambda,用来给算法传"比较规则" - 适配器 adaptors ------
stack/queue/priority_queue(容器适配器)、reverse_iterator、back_insert_iterator - 空间配置器 allocators ------
std::allocator,负责容器底层内存的申请与释放
收尾举个例子 :std::sort(vec.begin(), vec.end()) 和 std::sort(deq.begin(), deq.end()) 调用的是同一份算法代码,因为两者都提供随机访问迭代器------这就是泛型编程的价值。
高频追问:
vector扩容 :按 1.5 / 2 倍申请新空间 → 搬移元素 → 释放旧空间,均摊 O(1) ;扩容会让所有迭代器失效 ;reserve()可提前避免反复扩容。mapvsunordered_map:红黑树 vs 哈希表;前者有序、O(log n) ,后者无序、平均 O(1)、最坏 O(n) ;哈希冲突用链地址法 ,负载因子超阈值触发 rehash。deque:分段连续空间 + 中控数组,两端插入删除都是 O(1)。priority_queue:底层是vector+make_heap维护的大顶堆。
3.2 vector 和 list 的区别与选择
| 维度 | vector |
list |
|---|---|---|
| 结构 | 连续内存(动态数组) | 双向链表 |
| 随机访问 | O(1)(下标 / 迭代器加减) | 只能遍历 O(n) |
| 插入 / 删除 | 尾部均摊 O(1);中间 O(n) | 插入删除本身 O(1),但**"先找到位置"是 O(n)** |
| 迭代器失效 | 扩容后全部失效;insert / erase 后失效 | 只在删除对应元素时失效 |
| 内存 / 缓存 | 紧凑、缓存友好 | 每节点两个额外指针,缓存不友好 |
选择原则:
- 默认用
vector。因为内存局部性 / 缓存命中率的收益,通常远大于链表在"中间增删"上的理论优势。 - 需要大量随机访问 →
vector。 - 需要频繁在已定位位置插入删除 、或元素很大且要求引用稳定 →
list。 - 折中考虑
deque(两端 O(1) 且支持随机访问)。
⚠️ 别说"vector 不适合增删"------尾插恰恰是
vector最擅长的场景 (push_back均摊 O(1),reserve后更快)。
3.3 迭代器失效
以 vector 为例:
- 扩容 (
push_back/resize/reserve超出 capacity)→ 申请新内存 + 搬移元素 + 释放旧内存 → 所有 迭代器、指针、引用全部失效。 insert/erase→ 插入点 / 删除点之后 的迭代器失效(erase还使被删元素的迭代器失效),因为后续元素整体搬移。
规避方法:
- 插入删除后重新取
begin()/end()。 - 用下标代替迭代器。
- 先
reserve避免扩容。 - 边遍历边删:用
erase-remove惯用法,或用it = v.erase(it)的返回值续接。
对比 :list / map / set 的迭代器插入时不失效 ,删除时只有被删元素的失效;deque 是"部分失效"------两端插入一般只影响迭代器,中间插入全失效。
3.4 排序算法的稳定性
定义 :如果两个元素的关键值相等 ,排序前后它们的相对顺序保持不变,这个排序算法就是稳定的。
记忆口诀:
- 不稳定:快、选、堆、希 ------ 快速排序、选择排序、堆排序、希尔排序
- 稳定:冒泡、插入、归并、计数、桶、基数排序
快速排序为什么不稳定? 核心在 partition 里的元素交换会跨越相等的元素。
举例:[5a, 5b, 3],取 5a 作 pivot,从右侧找第一个小于 pivot 的元素(3)与 5a 交换 → [3, 5b, 5a]。5a 和 5b 的相对顺序被颠倒 → 不稳定。
稳定性为什么重要 (这半句能加分):多关键字排序时,先用次关键字排一遍,再用稳定排序按主关键字排一遍 ,就能得到"主序有序、同主序内部保持原有次序"的结果------基数排序正是靠这个原理。中间若用了不稳定排序,前一轮的结果会被破坏。
追问准备:
- 快排:平均 O(n log n),最坏 O(n²)(每次划分极不均衡,如已有序且取首元素为 pivot);三数取中 / 随机化 pivot 可缓解;空间 O(log n)(递归栈)。
- 归并:稳定 ,最坏也是 O(n log n),但需要 O(n) 额外空间。
- 堆排:不稳定,空间 O(1)。
std::sort用的是 introsort(快排 + 堆排 + 插入排序的组合),不稳定 ;需要稳定就用std::stable_sort。
四、编译与调试工具链
4.1 gcc 和 g++ 的区别
五个要点,按这个顺序答最清楚:
- 两者都是 GCC 的驱动程序。
- 本质区别 :
gcc按文件后缀决定语言 (.c→ C,.cpp→ C++),g++一律按 C++ 处理(不管后缀)。 g++会自动链接libstdc++(C++ 标准库),gcc默认不链 → 用gcc编 C++ 要显式加-lstdc++。- 由此带来的差异:C 和 C++ 在隐式类型转换、
void*转其他指针、函数重载 / 名字修饰、main返回值等规则上都不同,同一段代码用两种方式编译可能报不同的错。 - 另外可以用
gcc -x c++强制指定语言。
4.2 静态库与共享库
| 维度 | 静态库 .a / .lib |
共享库 .so / .dll |
|---|---|---|
| 链接时机 | 链接期把机器码合并进可执行文件 | 只记录依赖,运行时由动态链接器加载 |
| 体积 | 可执行文件大,每个程序一份副本 | 可执行文件小,物理内存里代码段只有一份(靠页表映射到各进程虚拟地址空间) |
| 升级 | 必须重新编译 | 可单独替换(但必须保证 ABI 兼容) |
| 加载开销 | 无 | 启动时重定位(-fPIC + 延迟绑定可缓解) |
| 风险 | 无 | 版本 / ABI 不兼容 → 依赖地狱 |
编译相关:
-fPIC生成位置无关代码(共享库必须)。- 链接时
-L<dir> -l<name>;默认优先.so,要静态链接加-static或-Wl,-Bstatic。 - 运行时找不到库:
LD_LIBRARY_PATH、rpath/$ORIGIN,或写进/etc/ld.so.conf后执行ldconfig。
4.3 怎么查看程序依赖了哪些共享库
ldd ./a.out ------ 最直接的答案。
相关命令(备着,面试官可能顺着问):
| 命令 | 用途 |
|---|---|
readelf -d |
看 ELF 的 NEEDED 条目 |
objdump -p |
看程序头 / 依赖 |
nm -D |
看动态符号表 |
patchelf |
修改 rpath / interpreter |
LD_DEBUG=libs ./a.out |
打印动态库加载过程 |
ldconfig -p |
查看系统库缓存 |
4.4 gdb 常用命令与多线程排死锁
启动方式 :gdb ./a.out、gdb ./a.out core(调 core 文件)、gdb -p <pid>(attach 运行中的进程)。
常用命令:
| 命令 | 作用 |
|---|---|
break(b) |
下断点(b file:line、b func、条件断点 b 12 if x>5) |
run(r) |
启动程序 |
continue(c) |
继续执行 |
next(n) |
单步,不进入函数 |
step(s) |
单步,进入函数 |
finish |
执行到当前函数返回 |
until |
运行到指定行 |
print(p) |
打印变量 / 表达式 |
x |
查看内存(x/16xb ptr) |
bt |
backtrace,查看调用栈 |
frame / up / down |
切换栈帧 |
info locals / info args |
查看局部变量 / 参数 |
info threads |
查看所有线程 |
watch |
监视点(变量被修改时中断) |
thread apply all bt |
打印所有线程的调用栈 |
多线程排死锁的完整姿势(面试里能讲出这一段很加分):
gdb -p <pid>attach 上去。info threads看线程列表。thread apply all bt打印全部调用栈,找哪个 / 哪些线程卡在pthread_mutex_lock。p mutex.__data.__owner拿到持锁线程的 TID,对照上一步找出持锁却不释放的线程。- 编译时要带
-g -O0(或-Og)才有符号;-fsanitize=thread(TSan)比 gdb 更适合抓竞态。
五、操作系统
5.1 进程与线程的区别
按四个维度答,条理最清楚:
| 维度 | 进程 | 线程 |
|---|---|---|
| 定位 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 资源 | 独立地址空间、页表、fd 表、信号处理 | 共享 进程的地址空间 / fd;但有独立的栈、寄存器、PC、TLS |
| 开销 | 创建 / 切换要切页表(TLB 刷新),重 | 轻,切换开销小 |
| 通信 | 必须 IPC | 直接读写共享内存 + 同步原语 |
| 健壮性 | 隔离性好,一个崩溃不影响其他 | 一个线程段错误 → 整个进程挂掉 |
再加一层:协程是用户态调度,切换不陷入内核,比线程更轻。
5.2 进程间通信(IPC)
五个必答 + 一个补充:
| 方式 | 说明 | 拷贝次数 |
|---|---|---|
| 管道(匿名管道 / 命名管道 FIFO) | 半双工,父子或无亲缘进程 | 2 次(用户 → 内核 → 用户) |
| 消息队列 | 有格式的消息链表,可随机读取 | 2 次 |
| 共享内存 | 最快,多个进程映射同一物理页 | 0 次(但要自己配同步) |
| 信号量 | 主要用于同步 / 互斥,常配共享内存 | --- |
| 套接字(Socket) | 可跨主机,本机也可用 AF_UNIX |
2 次 |
| 信号(signal) | 异步通知,信息量小 | --- |
要点 :共享内存最快 (省掉两次拷贝),代价是必须自己用信号量 / 互斥锁做同步。套接字 的 AF_UNIX 域比走 TCP 回环更快。
5.3 线程同步
四个必答 :互斥锁(mutex)、条件变量(condition_variable)、读写锁(rwlock)、信号量(semaphore)。
严格说还有:
- 自旋锁:忙等,适合临界区极短的场景(内核态常用)。
- 原子操作 :
std::atomic,无锁编程的基础。 - 屏障(barrier) 、
std::call_once。
必须记住的两个细节(面试官很爱追):
- 条件变量必须配互斥锁使用 :
wait()要在持有 mutex的情况下调用。 wait()必须放在while循环里而不是if------ 防虚假唤醒(spurious wakeup)。
cpp
std::unique_lock<std::mutex> lk(mtx);
while (queue.empty()) { // 必须是 while,不是 if
cv.wait(lk);
}
5.4 并行与并发
- 并发(concurrency) :宏观上同时、微观上交替 ------ 靠 CPU 时间片轮转让多个任务"看起来"在同时跑(单核也能并发)。目标是结构上解耦、提高吞吐和响应性。
- 并行(parallelism) :真正的同时执行 ------ 必须多核,多个任务在同一时刻物理上各占一个核。目标是缩短总耗时。
一句话记法 :并发是"一个人轮流处理多件事",并行是"多个人同时各做一件事"。并发是处理多个任务的能力 ,并行是同时执行多个任务的手段。两者不互斥------多核上的多线程程序既并发又并行。
5.5 CPU 亲和性(CPU affinity)
定义 :把一个进程 / 线程绑定到指定的一个或一组 CPU 核上运行。
好处:
- 减少跨核迁移 → 保住该核的 L1/L2 缓存热度和 TLB,降低性能抖动。
- 实时 / 低延迟任务避免被调度器迁移导致时间不确定。
- NUMA 架构下绑到就近节点,避免跨节点访存。
接口:
| 层面 | 方式 |
|---|---|
| C API | sched_setaffinity() / sched_getaffinity() |
| 命令行 | taskset -c 0-3 ./a.out |
| 线程级 | pthread_setaffinity_np |
| NUMA | numactl --cpunodebind=0 |
代价:绑得太死会导致负载不均,反而降低整体吞吐------所以生产上通常"绑一组核"或者干脆交给调度器。
5.6 IO 多路复用:select / poll / epoll
select |
poll |
epoll |
|
|---|---|---|---|
| fd 上限 | 1024 (FD_SETSIZE) |
无(链表) | 无(受 max_user_watches 内存限制) |
| 内核实现 | 位图 + 每次全量拷贝 | 数组 + 每次全量拷贝 | 红黑树 (存 fd)+ 就绪链表(rdllist) |
| 每次调用 | fd 集合整体从用户态拷贝进内核,内核线性扫描全部 fd | 同 select | fd 只注册一次(epoll_ctl),epoll_wait 不拷贝 |
| 返回后 | 用户态还要再遍历一遍才能知道谁就绪 | 同 select | 直接给出就绪 fd 列表 |
| 复杂度 | O(n) | O(n) | 检测 O(1) (回调挂就绪链表);epoll_wait 成本 O(就绪数),与总 fd 数无关 |
| 触发模式 | LT | LT | LT / ET |
epoll 的三个系统调用 :epoll_create1 → epoll_ctl(ADD / MOD / DEL) → epoll_wait。
⚠️ 一个常见错误:把 epoll 也说成 O(n)。那是"返回后遍历就绪链表"的成本(等于就绪 fd 的个数 ),不是 遍历全部 fd。这才是 epoll 在大规模连接下胜出的根本原因。
5.7 epoll 的 LT 与 ET
| LT 水平触发(默认) | ET 边缘触发 | |
|---|---|---|
| 通知时机 | 只要 fd 还有数据没读完 (或写缓冲仍可写),epoll_wait 反复通知 |
只在状态变化的那一刻 (无数据 → 有数据)通知一次 |
| 读法 | 可以一次只读一部分,下次还会提醒 | 必须循环 read / write 直到返回 EAGAIN |
| fd 要求 | 阻塞或非阻塞都行 | 必须 O_NONBLOCK,否则最后一次 read 会永久阻塞 |
| 优点 | 编程简单、不会丢事件 | 减少 epoll_wait 唤醒次数 → 高并发下系统调用更少、更高效 |
| 风险 | 就绪事件多时重复唤醒 | 漏读就永久卡住 |
编程上的差异(高频追问):
- ET :
listen_fd上的accept要写成while ((conn = accept(...)) > 0) {...},直到返回-1 && errno == EAGAIN;recv同理------一次事件要把缓冲区读干净。 - LT :可以只
accept一次、只recv一次;但因为不知道一次能读多少,工程上通常也写成非阻塞 + 循环读。
再把 EPOLLONESHOT 拎清楚(很容易和 LT/ET 混):
EPOLLONESHOT 让 fd 触发一次后被自动摘除 ,必须用 epoll_ctl(EPOLL_CTL_MOD) 手动重新武装。目的是保证同一条连接在同一时刻只被一个 worker 线程处理,避免多线程并发读写同一个 fd 造成数据错乱。
它和 LT / ET 是正交的两个概念 ,不要绑在一起讲。一个典型的组合是:
listen_fd用 LT +O_NONBLOCK(accept判EAGAIN退出),conn_fd用EPOLLIN | EPOLLONESHOT,业务处理完再epoll_ctl(EPOLL_CTL_MOD)重新武装。
5.8 Windows 下开发和 Linux 下开发的区别
这题没有标准答案,看你的维度层次。按五个维度答,条理感立刻不同:
- 工具链 :Linux 是
gcc/g++/clang+make/CMake+gdb/perf/valgrind;Windows 主流是 MSVC +cl.exe+ Visual Studio + WinDbg,也有 MinGW / clang-cl。Linux 工具链更开放、更容易脚本化和进 CI。 - 进程与 IO 模型 :Linux 一切皆文件,
fork/exec分离,IO 多路复用是 epoll ;Windows 是句柄 + 对象管理器,CreateProcess一体化,异步 IO 是 IOCP 。高并发服务端 Linux 生态更成熟。 - 编译产物与 ABI :Linux 产出 ELF /
.so/.a,Windows 是 PE /.dll/.lib;而且两边的 C++ ABI 不通用,不能跨平台直接传 STL 对象,调用约定也不同。 - 明暗差异:路径分隔符、字符编码(UTF-8 vs 本地代码页)、换行符 CRLF vs LF。
- 部署与生态:服务端绝大多数在 Linux 上,容器、CI/CD、云原生都以 Linux 为默认;Windows 的优势在桌面客户端、游戏和 .NET 生态。
⚠️ 永远不要贬低工具,要讲场景。 说"Windows 底层不适合开发"是错的------Windows 内核同样是工业级内核,只是服务端生态在 Linux 上。
六、计算机网络
6.1 什么是 socket(套接字)编程
一句话定义 :socket 是操作系统提供给应用程序的网络通信抽象接口 ,本质是内核里的一个通信端点 ,在进程里表现为一个文件描述符(fd)------对它读写就等于收发网络数据。
建立一次 TCP 通信的完整流程:
- 服务端 :
socket()创建 →bind()绑定 IP + 端口 →listen()进入监听态(内核维护连接队列)→accept()取出一个已完成三次握手的连接,返回新的 fd →recv()/send()→close() - 客户端 :
socket()→connect()(触发三次握手)→send()/recv()→close()
内核在这中间做了什么(加分点):
- 三次握手完全由内核协议栈自动完成,应用层感知不到。
- 每条连接由四元组(源 IP、源端口、目的 IP、目的端口)唯一标识,内核为它分配接收缓冲区和发送缓冲区。
accept()返回的是新 fd,指向这条具体连接;监听 fd 只负责等新连接。send()是把数据从用户态拷贝进 内核发送缓冲区,recv()是从接收缓冲区拷贝出来 ------所以 socket 通信的本质是"两次内存拷贝 + 协议栈处理"。
TCP 是字节流,没有消息边界 (高频追问):一次 send() 可能被拆成多个 TCP 段,一次 recv() 也可能只收到半个消息或两个半消息。必须自己在应用层定边界。
高并发场景怎么用 :一条连接一个线程会耗尽资源,所以用 IO 多路复用 ------把监听 fd 和所有连接 fd 注册进 epoll,epoll_wait 统一等待就绪事件,就绪后把任务投递给线程池处理;用 EPOLLONESHOT 保证同一条连接同一时刻只被一个 worker 处理。
阻塞 vs 非阻塞 :默认 recv() 无数据时阻塞;设 O_NONBLOCK 后无数据立即返回 EAGAIN,配合 ET 模式必须循环读到 EAGAIN 为止。
6.2 什么是 HTTP
第一句必须准 :HTTP = HyperText Transfer Protocol,超文本传输协议 ,是 TCP/IP 四层模型里的应用层协议 ,默认端口 80 (HTTPS 是 443 ),基于 TCP。
它解决什么问题 :定义客户端(浏览器)和服务器之间请求-响应报文的格式与语义------
- 报文结构:请求行 / 状态行 + 头部字段 + 空行 + 主体
- 请求方法:GET / POST / PUT / DELETE / HEAD / OPTIONS
- 状态码:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务端错误
两条核心性质:
- 无连接(1.0 时代:一次请求一次连接)
- 无状态 :服务器不记上次干了什么 → 所以需要 Cookie / Session / Token 维持登录态
一次请求的完整链路 :DNS 解析 → TCP 三次握手 →(HTTPS 再加 TLS 握手)→ 发请求 → 服务器处理并响应 → 四次挥手。HTTP/1.1 起默认 keep-alive 复用连接。
和 HTTPS 的关系 :HTTPS = HTTP + TLS,不是新协议。它在 HTTP 和 TCP 之间插入 TLS 层,负责证书校验、密钥协商、加密传输,解决明文传输的窃听和篡改问题。
版本演进:
| 版本 | 关键改进 | 遗留问题 |
|---|---|---|
| 1.1 | keep-alive 长连接、管线化 | 应用层队头阻塞 |
| 2 | 二进制分帧 + 多路复用(一个 TCP 连接并发多个流)+ HPACK 头部压缩 + 服务端推送 | 仍跑在 TCP 上,TCP 层队头阻塞没解决 |
| 3 | 基于 QUIC(UDP) ,可靠性 / 拥塞控制搬到用户态,支持 0-RTT 建连 | 生态与中间设备兼容性 |
6.3 TCP 和 UDP 的区别
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手 / 四次挥手) | 无连接 |
| 可靠性 | 确认应答、超时重传、校验和 | 不保证 |
| 传输形态 | 字节流(无消息边界 → 会粘包) | 数据报(保留边界,一块一块) |
| 顺序 | 保证有序(序列号重组) | 不保证 |
| 头部 | 20 字节起 | 8 字节 |
| 流量 / 拥塞控制 | 有 | 无 |
| 典型场景 | HTTP、文件传输、邮件、数据库连接 | DNS、DHCP、QUIC、音视频、直播、游戏 |
6.4 TCP 的可靠性是怎么保证的
⚠️ 高频陷阱 :千万别从"三次握手四次挥手"开头。三次握手 / 四次挥手解决的是"建立 / 释放连接",不是可靠性本身。 这样开头会被直接打断纠正。
可靠性靠这五条:
- 序列号 + 确认应答(ACK) ------ 每个字节都有序号,接收方确认已收到的连续序号。
- 超时重传 ------ RTO 自适应估算(RTT 平滑 + 偏差);还有快速重传:收到 3 个重复 ACK 立即重传,不等超时。
- 滑动窗口 ------ 流量控制 ------ 接收方通过窗口字段告诉发送方"我还能收多少",防止压垮对方。
- 拥塞控制四阶段 ------ 慢启动 → 拥塞避免 → 快重传 → 快恢复;用
cwnd感知网络状况。 - 校验和 + 乱序重排 + 去重 ------ 接收端按序号重组,丢弃重复包。
6.5 粘包与拆包怎么解决
先纠正一个前提 :粘包不是 TCP 的 bug,而是"TCP 是字节流、没有消息边界"的必然结果(Nagle 算法 / 合并发送 / 接收缓冲区堆积都会让它出现)。
四种解法:
- 定长消息 ------ 简单,但不灵活且浪费。
- 分隔符 ------ 如 HTTP 头用
\r\n\r\n、Redis 协议、SMTP。缺点:分隔符不能出现在正文,需要转义。 - 长度字段 + 包头(最通用) ------ 先读固定长度的包头 拿到 body 长度,再按长度读满;配合应用层缓冲区处理"半包"和"多包粘连"。
- 上层协议自带边界 ------ UDP(数据报)、WebSocket 帧、Protobuf / Thrift 的 length-prefix。
工程上的落点 :如果用 JSON 做通信协议,因为 JSON 是文本、本身没有边界,所以必须先定边界才能解析------这就是"定长包头 + 长度字段"存在的意义。
顺带必背的连接管理三件套:
| 方案 | 特点 |
|---|---|
| 短连接 | 一次请求一次连接,实现简单,但三次握手 + 四次挥手开销大 |
| 长连接 | 复用连接,省掉握手开销,需要心跳保活 |
| 连接池 | 预先建好 N 条长连接并复用,省掉握手 + 认证 + 上下文开销,是服务端最常用的做法 |
6.6 OSI 七层与 TCP/IP 四层
OSI 七层口诀:应表会传网数物。
| 层 | 代表协议 / 设备 |
|---|---|
| 应用层 | HTTP / HTTPS、DNS、FTP、SMTP、SSH |
| 表示层 | 编码 / 加密 / 压缩(TLS 常被归到这里) |
| 会话层 | 建立 / 管理 / 终止会话 |
| 传输层 | TCP、UDP、QUIC(还有 SCTP) |
| 网络层 | IP、ICMP、ARP、路由、路由器 |
| 数据链路层 | 以太网、MAC、交换机、VLAN |
| 物理层 | 网线、光纤、网卡、集线器 |
TCP/IP 四层:应用层(含 OSI 的上三层)→ 传输层 → 网络层 → 网络接口层。
6.7 常见端口号
| 服务 | 端口 | 服务 | 端口 |
|---|---|---|---|
| HTTP | 80 | HTTPS | 443 |
| MySQL | 3306 | Redis | 6379 |
| SSH | 22 | FTP | 21(控制)/ 20(数据) |
| SMTP | 25 | DNS | 53(TCP + UDP) |
| MongoDB | 27017 | PostgreSQL | 5432 |
| SQL Server | 1433 | Memcached | 11211 |
⚠️ 这题是送分题,答不出来非常难看。 尤其是 MySQL 的 3306 和 Redis 的 6379------只要简历里写了数据库和 Redis,就一定会被问。
七、数据库
7.1 事务与 ACID
事务 :一组要么全部成功、要么全部回滚的数据库操作,是一个不可分割的逻辑工作单元。
ACID 四大特性(必须做到条件反射):
| 字母 | 名字 | 含义 | 靠什么实现 |
|---|---|---|---|
| A | Atomicity 原子性 | 要么全做,要么全不做 | undo log 回滚 |
| C | Consistency 一致性 | 事务前后数据完整性约束不被破坏(目的) | 靠 A/I/D 共同保证 + 约束 / 触发器 |
| I | Isolation 隔离性 | 并发事务互不干扰 | 锁 + MVCC |
| D | Durability 持久性 | 提交后永久生效,崩溃也不丢 | redo log(WAL) |
⚠️ 最容易卡住的是 C(Consistency 一致性)。A、I、D 都很好记,唯独 C 的英文单词想不起来------而这个恰好是面试官最爱考的那一个。
7.2 隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 Read Uncommitted | ✓ 可能 | ✓ 可能 | ✓ 可能 |
| 读已提交 Read Committed | ✗ | ✓ 可能 | ✓ 可能 |
| 可重复读 Repeatable Read(MySQL InnoDB 默认) | ✗ | ✗ | ✓ 可能(InnoDB 用间隙锁 / Next-Key Lock 缓解) |
| 串行化 Serializable | ✗ | ✗ | ✗ |
三个概念:
- 脏读 :读到了别的事务未提交的数据。
- 不可重复读 :同一事务内两次读同一行,结果不同(别人改了)。
- 幻读 :同一事务内两次范围查询,第二次多出了行(别人插了)。
7.3 面试常考的"防超卖"怎么写
用一条原子 SQL 把"判断 + 更新"合并,杜绝"先查后改"的竞态窗口:
sql
UPDATE ticket
SET count = count + 1
WHERE tk_id = ? AND count < tk_max;
为什么能防超卖(面试官一定会追的三连问):
- 为什么这样写就安全? 判断(
count < tk_max)与更新(count = count + 1)在同一条 SQL 里,由数据库保证原子性,不存在"查完再改"之间的时间窗。 - 并发怎么串行化的? 更新的是同一行 → 走行锁 ,并发请求被串行化。后到的请求看到的是已经更新过的 count,因此不会重复售出。
- 怎么知道有没有卖出去? 看
affected_rows:返回 0 表示条件不满足(票已售完),此时直接rollback。
配套要把事务边界讲清楚 :从"检查余量 / 插入预约记录"到"更新 count",整体包在一个事务里,失败 rollback、成功 commit。
如果用了连接池,还有个必答点 :借出的同一条连接上执行 BEGIN...COMMIT,归还前必须确保事务已结束,否则事务会串到下一个使用者身上。
7.4 视图与基本表的区别
| 基本表 | 视图 | |
|---|---|---|
| 本质 | 真实存在、物理存储数据 | 虚拟表 ,只存 SQL 定义,不存数据 |
| 存储 | 占磁盘 | 只有元数据 |
| 索引 | 可以建 | 不能直接建(物化视图除外) |
| 可更新 | 是 | 受限 |
视图的优点:
- 简化复杂查询 ------ 把多表 join 封装成一个名字。
- 安全 / 权限控制 ------ 只暴露部分行 / 列,不用把整张表给出去。
- 逻辑独立性 ------ 底层表结构变了,改视图、应用不用动。
视图的缺点:
- 性能差 ------ 视图的数据实际来自基本表,等于在基本表查询之上再做一次查询,谓词常常无法下推。
- 可更新性受限 ------ 含聚合 /
DISTINCT/GROUP BY/ 多表 join 的视图一般不可更新;简单视图(单表、无聚合)可以更新,会直接改到底层表。 - 依赖脆弱 ------ 底层表改了视图可能失效。
延伸:MySQL 没有原生物化视图 (要靠定时任务 / 触发器自己实现);WITH CHECK OPTION 用来控制通过视图更新时是否必须满足视图条件。
八、Redis
8.1 Redis 是什么
一句话 :Redis = REmote DIctionary Server,一个基于内存的 key-value(NoSQL)数据库 ,用 C 写,单线程处理命令 + IO 多路复用(epoll) ,常用于缓存、分布式锁、计数器、排行榜、消息队列。
为什么快:
- 纯内存操作。
- 单线程处理命令 → 无锁、无上下文切换。
- IO 多路复用(epoll) 支撑高并发连接。
- 数据结构针对场景做了高度优化(SDS、跳表、listpack)。
8.2 五种基本类型与底层结构
| 类型 | 典型用途 | 底层结构 |
|---|---|---|
| String | 缓存对象、计数器(INCR)、分布式锁(SET NX EX) |
SDS(动态字符串) |
| List | 消息队列、时间线 | quicklist(双向链表 + listpack) |
| Hash | 存对象字段 | 哈希表 / listpack |
| Set | 去重、共同好友(交并集) | 哈希表 / intset |
| ZSet(Sorted Set) | 排行榜、延时队列 | 跳表 skiplist + 哈希表 |
新类型(Redis 5+) :BitMap、HyperLogLog(基数统计)、GEO(地理位置) ;Redis 6 增加了 Stream(消息队列)。
⚠️ 常见失误:把 ZSet 的用途 (排行榜 / 地理位置)当成类型名 来说。先把五个英文名背顺:String / List / Hash / Set / ZSet。
8.3 RDB 和 AOF 的区别
⚠️ 这一题最容易张冠李戴 ------把"快照、两次快照之间丢数据"说成 AOF 的特征。那是 RDB 的。
| 维度 | RDB | AOF |
|---|---|---|
| 形式 | 快照 :把某一时刻的整份内存数据写成二进制文件 | 命令日志 :把每条写命令追加写进文件 |
| 触发 | save / bgsave / 配置 save 900 1 |
appendfsync always / everysec / no |
| 恢复速度 | 快(直接 load 二进制) | 慢(要重放所有命令) |
| 文件大小 | 小(压缩二进制) | 大(文本命令,可 rewrite 重写瘦身) |
| 丢数据 | 可能丢两次快照之间的全部数据 | everysec 最多丢 1 秒;always 不丢但很慢 |
| 性能影响 | fork 子进程做快照,fork 瞬间有阻塞 | 每次写都要落盘,always 模式影响大 |
| 适合 | 冷备 / 灾备、大数据量快速恢复 | 数据安全要求高的场景 |
生产实践:混合持久化 (Redis 4.0+ 的 aof-use-rdb-preamble yes)------AOF 重写时前半段用 RDB 格式写全量快照 ,后半段追加增量命令,兼顾恢复速度 与丢数据量。
默认状态 :默认配置里 RDB 开启、AOF 关闭。
延伸准备 :RDB 的 fork + 写时复制(COW) ;AOF 的 rewrite 与 aof_buf 缓冲、fsync 时机。
8.4 缓存穿透、击穿、雪崩
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 穿透 | 查一个数据库里也不存在的 key,每次都打到 DB | 布隆过滤器 或 缓存空值 |
| 击穿 | 热点 key 突然过期,瞬间大量请求打到 DB | 互斥锁重建 或 逻辑过期 + 异步更新 |
| 雪崩 | 大批 key 同时过期 / Redis 宕机 | 过期时间加随机扰动 + 多级缓存 + 限流降级 |
顺带一个高频追问:本地缓存和 Redis 有什么区别?
| 本地缓存(进程内,如 LRU map、Caffeine) | Redis | |
|---|---|---|
| 性能 | 无网络开销,纳秒级 | 有网络 RTT(0.1--1 ms) |
| 容量 | 受单机内存限制 | 大 |
| 一致性 | 多实例之间不一致 | 集中式、一致 |
| 持久化 | 进程重启即失效 | 可持久化 |
生产上的做法是两级缓存:本地缓存挡最热的 key,Redis 兜住全量。
九、高频常数速查表
这一节纯属"背了就得分",建议每天过一遍。
9.1 端口号
| 服务 | 端口 | 服务 | 端口 |
|---|---|---|---|
| HTTP | 80 | HTTPS | 443 |
| MySQL | 3306 | Redis | 6379 |
| SSH | 22 | FTP | 21 / 20 |
| SMTP | 25 | DNS | 53 |
| MongoDB | 27017 | PostgreSQL | 5432 |
| SQL Server | 1433 | Memcached | 11211 |
9.2 大小与常量
| 项 | 值 |
|---|---|
| 空类对象 | 1 字节 |
| vptr(64 位 / 32 位) | 8 / 4 字节 |
| 指针(64 位) | 8 字节 |
| TCP 头 / UDP 头 | 20 字节起 / 8 字节 |
select 的 fd 上限 |
1024 (FD_SETSIZE) |
TIME_WAIT 时长 |
2MSL |
std::sort 的算法 |
introsort(快排 + 堆排 + 插排),不稳定 |
9.3 复杂度
| 操作 | 复杂度 |
|---|---|
vector 随机访问 / 尾插 |
O(1) / 均摊 O(1) |
vector 中间插入删除 |
O(n) |
list 插入删除(已知位置) |
O(1) |
map / set 查找 |
O(log n) |
unordered_map 查找 |
平均 O(1),最坏 O(n) |
| 快排 / 归并 / 堆排 | 平均 O(n log n)(快排最坏 O(n²)) |
select / poll |
O(n) |
epoll 检测 / epoll_wait |
O(1) / O(就绪数) |
9.4 稳定性口诀
不稳定:快、选、堆、希 ------ 快速排序、选择排序、堆排序、希尔排序
稳定:冒泡、插入、归并、计数、桶、基数
9.5 其他必背口诀
- OSI 七层:应表会传网数物
- IPC 五种:管道、消息队列、共享内存、信号量、套接字(+ 信号)
- 线程同步四种:互斥锁、条件变量、读写锁、信号量
- 拥塞控制四阶段:慢启动 → 拥塞避免 → 快重传 → 快恢复
- STL 六大组件:容器、算法、迭代器、仿函数、适配器、空间配置器
十、面试经验:几条比知识点更容易踩的坑
知识点可以补,但下面这几条是"知道也照样失分"的地方,性价比最高。
10.1 名词题:第一句话必须准
面试官问"什么是 X"的时候,他要的不是长篇展开,而是"你有没有第一句话就能准确定义"的能力------这是判断"真学过"还是"背过题"最省时间的手段。
所以每个名词都准备 "全称 + 中文 + 一句话定位 + 3 个关键词" ,并且限时 5 秒内开口。
最容易卡住的往往是这种:
- ACID 里的 C(Consistency 一致性)------记住 A、I、D 却想不起 C,非常常见。
- Redis 五种类型的英文名 ------
String / List / Hash / Set / ZSet,说中文能说、说英文就卡。 select/poll/epoll的时间复杂度。
一句原话很值得记住:代码是一个很严谨的东西,这种名词你不能说错。
10.2 只追一层就够,但要准备好第二层
多数一面不会把一个问题追到很深------它只要第一层定义。所以"把是什么说准"就已经拿到分。
但二面会追第二层("为什么"、"如果换成 X 呢"、"你项目里为什么这么选")。典型会断在第二层的问题:
- 抽象类为什么不能实例化?
- vtable 到底存在哪里?
- ET 模式在编程上要注意什么?
- 迭代器失效的具体规则?
- 排序为什么要关心稳定性?
准备方式:每个知识点写三层。第一层定义 → 第二层原理 → 第三层"我的项目里是怎么用的"。
10.3 不要自造词
这是最伤的一条。
- ❌ "有数据就警报 " → ✅ "通知 (notify)/ 就绪(ready)"
- ❌ "动态重载 就是虚函数" → ✅ "动态多态通过虚函数实现"
- ❌ "临行继承 " → ✅ "菱形继承"
术语不准,面试官会立刻把你归到"没系统学过"那一类。面试里宁可说得朴素,也不要用不确定的词。
10.4 不要答出一种就认输
被问到"XX 有几种做法"时,很多人答出第一种就自动补一句"我只想到这一种"。这句话比答不全更致命。
花 5 秒把能想到的都列出来,然后坦白哪些有实战经验、哪些只是了解:
"常见的有定长、分隔符、长度字段三种,我项目里用的是长度字段。另两种我了解思路,但没在项目里用过。"
答出 1 种不可怕,答出 1 种就认输才可怕。
10.5 不会的题怎么答
不要沉默,也不要瞎猜。标准话术:
"这个概念我现在说不准,但我知道它属于 XX 领域,跟 YY 有关系,我回去会马上补上。"
承认不丢分,沉默和瞎猜才丢分。
被纠正时的标准反应:
"您说得对,我刚才那句话表述错了,正确的是......"
不要重复上一句,也不要辩解。
10.6 一面的筛子和二面的筛子不一样
- 一面 = 筛基础 。一面的目的只是确认常规基础题你能答出来 。它不会问特别难的问题------难题区分不出人。所以基础题答不好,杀伤力最大,因为那是唯一能拿分的地方。
- 二面 = 问项目 + 拓展题 。会让你讲思路、问框架、问"平时用过但这时候得问为啥"。靠背八股是过不了的 ,要的是理解 + 能应用。
所以复习顺序应该是:先保证一面能过(把定义和常识打牢)→ 再准备二面(把项目讲深)。
10.7 项目要被追问的五个问题
先把这五个问题的答案写好,每个准备 60 秒版本:
- 你为什么想着写这样一个项目?
- 你写这个项目花了多长时间?
- 最大的难点在哪里 / 遇到的最大困难是什么?
- 这个项目你是怎么测试的?(数字要能溯源:QPS 怎么压的、多少并发、什么机器)
- 哪部分是你自己写的?实现了哪些功能?
⚠️ 只写你真的写过的。 凡是写在简历上的,都要能扛三层追问。编出来的东西撑不过两轮。
10.8 软性问题背后的两个真实考察点
所有"你家是哪里的""有没有对象""以后什么规划"这类闲聊,本质上都在判断同一件事:你来了之后会怎么走。可以归纳成两个问题:
- 能不能来------他会从侧面绕一圈打探:籍贯、家人态度、感情状况、对目标城市的了解程度。绕这么多就是不直接问,因为直接问你会给标准答案。
- 能干多久 ------开发岗前面一段时间产出有限,通常要到两三年后才进入产出期,所以公司天然偏好待得久的人。问你未来规划,其实就是在问这个。
所以回答时的原则是:给具体的依据,而不是表态。
- ❌ "我对陌生环境的适应能力比较强,我有信心。"(面试官听过一万遍)
- ✅ "我高中起就在外地,大学四年独立生活,租房、通勤、自己做饭这些对我不是障碍;家里也沟通过,他们是支持的。"
关于规划 :表达专注技术路线通常是最稳的("想在这条技术路线上一直走下去")。刚毕业就说想转管理 / 销售 / 产品,容易被判定为"在技术路线上走不长"。
10.9 薪资怎么谈
几个通用原则:
- 对方先问你期望薪资时,不要急着报数。 让对方先说区间,你再在这个区间里定位。
- 报数要有依据。 做过市场调研(同城市、同岗位、同经验区间的行情),或手里有真实 offer 作为锚点------而不是"我觉得"。
- 手上有 offer 才谈得硬气。 有一份保底 offer 在手,谈判时更从容,对方也能感觉到你不是随便投投。
- 别为了小差距反复折腾。 如果两家差一两千,跳过去要重新适应、重新证明自己,通常不划算;差距不明显就不值得动。
- 不要过早问加班、加班费等敏感问题。 时机很重要------在还没确定给你发 offer 之前问,容易被解读成"你对这份工作有意见" 。等进入谈 offer / 签三方阶段再问,换个不冒犯的问法:"团队平时的工作节奏大概是怎样的?"
10.10 自我介绍和反问,是唯二能自己掌控的环节
自我介绍 只有一两分钟,但这是唯一一段完全由你控制的时间 。不要复述简历,简历上有的人家自己会看。要讲简历上"不好写、但值得说"的东西:
- 学校期间做过的一些小系统、小工具(简历放不下,但讲了能证明你在动手)
- 一些能体现动手量的细节,比如"把某本经典书里的数据结构和算法全部手敲了一遍"
- 技术栈的整体感:让他觉得"这个人从大学开始一直在学"
反问环节:不管面试官问不问你"还有什么想问的",都可以主动问。
一个几乎不会出错的问题:
"如果我想往 XX 方向(对方岗位的方向)发展,您觉得我还应该重点补哪些能力?"
好处是:既显得你有明确的职业意图,又能把面试官的话变成你下一轮复习的清单------他说的内容不重要,重要的是他愿意说。
结语
这套题整理下来,最大的感受是:面试考的不是"你会不会",而是"你能不能在压力下把会的东西准确地说出来"。
所以复习的终点不是"看完",而是"能出声讲一遍 "。一个便宜又有效的办法:把这份清单当成题库,找个人互相问------你问他、他用自己的话讲,你一听就知道"他这么讲合适吗",反过来自己讲的时候也能立刻发现自己卡在哪。
希望这份整理对准备 C++ 后端方向的你有用。文中有任何说得不准的地方,欢迎在评论区指正。