C++ 后端开发面试题整理

C++ 后端开发面试题整理

最近把 C++ / Linux 后端方向面试里被反复问到的问题按模块整理了一遍,去掉了一些零散的口水话,留下"能直接复习"的部分。内容偏校招 / 实习难度,覆盖 C++ 语言、内存管理、STL、编译工具链、操作系统、计算机网络、数据库和 Redis,最后附一份高频常数速查表和几条比知识点更容易踩的坑。

适合谁看:准备 C++ 后端方向的校招 / 实习,已经会写代码但"名词题第一句话说不准""一被追问第二层就断"的同学。

食用方式 :每个题目只用一句话给出核心定义,后面才是展开,建议先盖住答案自己说一遍。面试里真正决定分数的不是"你知不知道",而是"你能不能在三秒内把第一句说准"。


目录


一、C++ 语言基础

1.1 什么是函数重载?

一句话 :同一作用域内、函数名相同、参数列表不同 的多个函数,与返回类型无关。

  • 参数列表不同体现在类型 / 个数 / 顺序,任何一项不同就构成重载。
  • 编译期由名称修饰(name mangling)生成不同符号完成决议,属于静态多态。
  • C 语言不支持重载,因为 C 的符号不做名字修饰。
  • 常见误区:只改返回类型不构成重载;const 成员函数与非 const 版本可以重载(因为隐含的 this 类型不同)。

⚠️ 别把"编译期决议"说成"预编译阶段"。预处理阶段只做文本替换 ,重载决议发生在编译期。

1.2 指针和引用的区别

维度 指针 引用
本质 一个对象,存地址,x64 下占 8 字节 不是独立对象,是别名;但底层实现通常就是一个指针
能否为空 可以 nullptr 不能为空,必须初始化且绑定后不能改绑
多级 可以 int** 没有多级引用(引用折叠得到的是左值/右值引用,跟"多级"不是一回事)
sizeof 8(x64) 被引用对象的大小
&r 得到指针自身的地址 得到被引用对象的地址
++ 移动到下一个元素 语法上合法,但作用在被引用对象上,没有"指向下一个"的语义

两个高频陷阱:

  1. 说"引用不占空间"是不严谨 的,正确的说法是"引用在语言层面不是独立对象"。
  2. "引用的引用 = 将亡值 / 右值引用"是错的 。标准里"引用的引用"通过引用折叠得到左值引用或右值引用,和"将亡值"是两个概念。

1.3 #define 和 typedef 的区别

一句话 :#define 是预处理指令 ,做纯文本替换 ;typedef 是编译期 的类型别名,参与语法和类型检查。

维度 #define typedef
处理阶段 预处理期,文本替换 编译期,编译器理解
类型检查 无 有语法 / 语义检查
作用域 无(文件级,直到 #undef) 遵循 C++ 作用域
调试信息 看不到宏名 调试器里有别名
经典陷阱 #define PINT int* → PINT a,b; 只有 a 是指针 typedef int* PINT; → a、b 都是指针

#define 的三类用途与各自的坑:

  1. 常量宏 #define MAX_SIZE 1024:无类型、调试器看不到符号、报错报的是字面量。现在推荐 constexpr / const。
  2. 带参宏 #define MAX(a,b) ((a)>(b)?(a):(b)):参数和整体都必须加括号 ,否则 MAX(a+1,b) 展开后优先级出错;而且参数会被求值两次 ,传 MAX(i++, j) 会 double increment。
  3. 条件编译 #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; 是纯虚函数 ;含纯虚函数的类是抽象类。
  • 抽象类为什么不能实例化?
    1. 该类的 vtable 里对应槽位是未绑定的 (指向 __cxa_pure_virtual 之类的占位),一旦允许创建对象,通过基类指针调用它就必然崩。
    2. 语义上抽象类表达的是不完整的接口契约 ,标准直接禁止:error: cannot declare variable to be of abstract type。
  • 补充:抽象类可以定义指针和引用 ;纯虚函数可以给定义 (派生类可显式 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 起的初始化捕获,用于移动进闭包

最容易答错的三个点:

  1. 全局变量和静态变量不需要也不能被捕获 ------ 它们在静态存储区,任何地方都能直接访问。lambda 只捕获自动存储期变量 (局部变量、函数参数)。所以"lambda 能捕获全局变量吗"的正确答案是:能访问,但不叫捕获。
  2. 捕获的局部变量是副本。想改本体必须用引用捕获。
  3. 引用捕获要小心悬垂 ------ 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 三种申请内存的方式

  1. new T / new T(args) ------ 默认走全局 operator new,底层是 malloc。
  2. 定位 new(placement new) :new (ptr) T(args) ------ 在已有内存 上构造对象,不申请内存。典型用途是内存池、容器预留空间后的就地构造。
  3. 自己重载 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 个就永远没人能用------这就是泄漏。

两个容易被追问的层面(一定要分清):

  1. 进程运行期间 :堆内存由程序员(或分配器)管理,OS 不会主动回收。泄漏会持续累积,最终 OOM 或被 OOM Killer 杀掉。这就是"系统不管"的意思。
  2. 进程终止之后 :OS 会回收该进程的全部资源(地址空间、页表、打开的 fd),因为进程的虚拟地址空间本身就被销毁了。

所以完整说法是:"泄漏指的是进程生命周期内的浪费;进程一结束,泄漏的内存自然也被 OS 收回了。但长生命周期的服务不能靠重启解决,泄漏会拖垮机器。"

排查工具 :valgrind --leak-check=full、ASan(-fsanitize=address)、mtrace、观察 /proc/<pid>/status 里 VmRSS 的趋势。


三、STL

3.1 STL 六大组件

一句话 :STL = Standard Template Library,标准模板库,是 C++ 标准库中基于模板 实现的泛型编程 基础设施------核心思想是把数据结构和算法解耦,靠迭代器把两者粘起来。

六个组件(记这六个数):

  1. 容器 containers ------ vector / list / deque(序列),map / set(红黑树,有序),unordered_map / unordered_set(哈希,无序)
  2. 算法 algorithms ------ sort / find / transform / lower_bound,全是模板函数,只依赖迭代器,不依赖具体容器
  3. 迭代器 iterators ------ 容器与算法之间的粘合层;分五类:输入、输出、前向、双向、随机访问
  4. 仿函数 functors ------ std::less<T>、std::greater<T>、lambda,用来给算法传"比较规则"
  5. 适配器 adaptors ------ stack / queue / priority_queue(容器适配器)、reverse_iterator、back_insert_iterator
  6. 空间配置器 allocators ------ std::allocator,负责容器底层内存的申请与释放

收尾举个例子 :std::sort(vec.begin(), vec.end()) 和 std::sort(deq.begin(), deq.end()) 调用的是同一份算法代码,因为两者都提供随机访问迭代器------这就是泛型编程的价值。

高频追问:

  • vector 扩容 :按 1.5 / 2 倍申请新空间 → 搬移元素 → 释放旧空间,均摊 O(1) ;扩容会让所有迭代器失效 ;reserve() 可提前避免反复扩容。
  • map vs unordered_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++ 的区别

五个要点,按这个顺序答最清楚:

  1. 两者都是 GCC 的驱动程序。
  2. 本质区别 :gcc 按文件后缀决定语言 (.c → C,.cpp → C++),g++ 一律按 C++ 处理(不管后缀)。
  3. g++ 会自动链接 libstdc++ (C++ 标准库),gcc 默认不链 → 用 gcc 编 C++ 要显式加 -lstdc++。
  4. 由此带来的差异:C 和 C++ 在隐式类型转换、void* 转其他指针、函数重载 / 名字修饰、main 返回值等规则上都不同,同一段代码用两种方式编译可能报不同的错。
  5. 另外可以用 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 打印所有线程的调用栈

多线程排死锁的完整姿势(面试里能讲出这一段很加分):

  1. gdb -p <pid> attach 上去。
  2. info threads 看线程列表。
  3. thread apply all bt 打印全部调用栈,找哪个 / 哪些线程卡在 pthread_mutex_lock。
  4. p mutex.__data.__owner 拿到持锁线程的 TID,对照上一步找出持锁却不释放的线程。
  5. 编译时要带 -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。

必须记住的两个细节(面试官很爱追):

  1. 条件变量必须配互斥锁使用 :wait() 要在持有 mutex的情况下调用。
  2. 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 核上运行。

好处:

  1. 减少跨核迁移 → 保住该核的 L1/L2 缓存热度和 TLB,降低性能抖动。
  2. 实时 / 低延迟任务避免被调度器迁移导致时间不确定。
  3. 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 下开发的区别

这题没有标准答案,看你的维度层次。按五个维度答,条理感立刻不同:

  1. 工具链 :Linux 是 gcc / g++ / clang + make / CMake + gdb / perf / valgrind;Windows 主流是 MSVC + cl.exe + Visual Studio + WinDbg,也有 MinGW / clang-cl。Linux 工具链更开放、更容易脚本化和进 CI。
  2. 进程与 IO 模型 :Linux 一切皆文件,fork / exec 分离,IO 多路复用是 epoll ;Windows 是句柄 + 对象管理器,CreateProcess 一体化,异步 IO 是 IOCP 。高并发服务端 Linux 生态更成熟。
  3. 编译产物与 ABI :Linux 产出 ELF / .so / .a,Windows 是 PE / .dll / .lib;而且两边的 C++ ABI 不通用,不能跨平台直接传 STL 对象,调用约定也不同。
  4. 明暗差异:路径分隔符、字符编码(UTF-8 vs 本地代码页)、换行符 CRLF vs LF。
  5. 部署与生态:服务端绝大多数在 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()

内核在这中间做了什么(加分点):

  1. 三次握手完全由内核协议栈自动完成,应用层感知不到。
  2. 每条连接由四元组(源 IP、源端口、目的 IP、目的端口)唯一标识,内核为它分配接收缓冲区和发送缓冲区。
  3. accept() 返回的是新 fd,指向这条具体连接;监听 fd 只负责等新连接。
  4. 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 的可靠性是怎么保证的

⚠️ 高频陷阱 :千万别从"三次握手四次挥手"开头。三次握手 / 四次挥手解决的是"建立 / 释放连接",不是可靠性本身。 这样开头会被直接打断纠正。

可靠性靠这五条:

  1. 序列号 + 确认应答(ACK) ------ 每个字节都有序号,接收方确认已收到的连续序号。
  2. 超时重传 ------ RTO 自适应估算(RTT 平滑 + 偏差);还有快速重传:收到 3 个重复 ACK 立即重传,不等超时。
  3. 滑动窗口 ------ 流量控制 ------ 接收方通过窗口字段告诉发送方"我还能收多少",防止压垮对方。
  4. 拥塞控制四阶段 ------ 慢启动 → 拥塞避免 → 快重传 → 快恢复;用 cwnd 感知网络状况。
  5. 校验和 + 乱序重排 + 去重 ------ 接收端按序号重组,丢弃重复包。

6.5 粘包与拆包怎么解决

先纠正一个前提 :粘包不是 TCP 的 bug,而是"TCP 是字节流、没有消息边界"的必然结果(Nagle 算法 / 合并发送 / 接收缓冲区堆积都会让它出现)。

四种解法:

  1. 定长消息 ------ 简单,但不灵活且浪费。
  2. 分隔符 ------ 如 HTTP 头用 \r\n\r\n、Redis 协议、SMTP。缺点:分隔符不能出现在正文,需要转义。
  3. 长度字段 + 包头(最通用) ------ 先读固定长度的包头 拿到 body 长度,再按长度读满;配合应用层缓冲区处理"半包"和"多包粘连"。
  4. 上层协议自带边界 ------ 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;

为什么能防超卖(面试官一定会追的三连问):

  1. 为什么这样写就安全? 判断(count < tk_max)与更新(count = count + 1)在同一条 SQL 里,由数据库保证原子性,不存在"查完再改"之间的时间窗。
  2. 并发怎么串行化的? 更新的是同一行 → 走行锁 ,并发请求被串行化。后到的请求看到的是已经更新过的 count,因此不会重复售出。
  3. 怎么知道有没有卖出去? 看 affected_rows:返回 0 表示条件不满足(票已售完),此时直接 rollback。

配套要把事务边界讲清楚 :从"检查余量 / 插入预约记录"到"更新 count",整体包在一个事务里,失败 rollback、成功 commit。

如果用了连接池,还有个必答点 :借出的同一条连接上执行 BEGIN...COMMIT,归还前必须确保事务已结束,否则事务会串到下一个使用者身上。

7.4 视图与基本表的区别

基本表 视图
本质 真实存在、物理存储数据 虚拟表 ,只存 SQL 定义,不存数据
存储 占磁盘 只有元数据
索引 可以建 不能直接建(物化视图除外)
可更新 是 受限

视图的优点:

  1. 简化复杂查询 ------ 把多表 join 封装成一个名字。
  2. 安全 / 权限控制 ------ 只暴露部分行 / 列,不用把整张表给出去。
  3. 逻辑独立性 ------ 底层表结构变了,改视图、应用不用动。

视图的缺点:

  1. 性能差 ------ 视图的数据实际来自基本表,等于在基本表查询之上再做一次查询,谓词常常无法下推。
  2. 可更新性受限 ------ 含聚合 / DISTINCT / GROUP BY / 多表 join 的视图一般不可更新;简单视图(单表、无聚合)可以更新,会直接改到底层表。
  3. 依赖脆弱 ------ 底层表改了视图可能失效。

延伸:MySQL 没有原生物化视图 (要靠定时任务 / 触发器自己实现);WITH CHECK OPTION 用来控制通过视图更新时是否必须满足视图条件。


八、Redis

8.1 Redis 是什么

一句话 :Redis = REmote DIctionary Server,一个基于内存的 key-value(NoSQL)数据库 ,用 C 写,单线程处理命令 + IO 多路复用(epoll) ,常用于缓存、分布式锁、计数器、排行榜、消息队列。

为什么快:

  1. 纯内存操作。
  2. 单线程处理命令 → 无锁、无上下文切换。
  3. IO 多路复用(epoll) 支撑高并发连接。
  4. 数据结构针对场景做了高度优化(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 秒版本:

  1. 你为什么想着写这样一个项目?
  2. 你写这个项目花了多长时间?
  3. 最大的难点在哪里 / 遇到的最大困难是什么?
  4. 这个项目你是怎么测试的?(数字要能溯源:QPS 怎么压的、多少并发、什么机器)
  5. 哪部分是你自己写的?实现了哪些功能?

⚠️ 只写你真的写过的。 凡是写在简历上的,都要能扛三层追问。编出来的东西撑不过两轮。

10.8 软性问题背后的两个真实考察点

所有"你家是哪里的""有没有对象""以后什么规划"这类闲聊,本质上都在判断同一件事:你来了之后会怎么走。可以归纳成两个问题:

  1. 能不能来------他会从侧面绕一圈打探:籍贯、家人态度、感情状况、对目标城市的了解程度。绕这么多就是不直接问,因为直接问你会给标准答案。
  2. 能干多久 ------开发岗前面一段时间产出有限,通常要到两三年后才进入产出期,所以公司天然偏好待得久的人。问你未来规划,其实就是在问这个。

所以回答时的原则是:给具体的依据,而不是表态。

  • ❌ "我对陌生环境的适应能力比较强,我有信心。"(面试官听过一万遍)
  • ✅ "我高中起就在外地,大学四年独立生活,租房、通勤、自己做饭这些对我不是障碍;家里也沟通过,他们是支持的。"

关于规划 :表达专注技术路线通常是最稳的("想在这条技术路线上一直走下去")。刚毕业就说想转管理 / 销售 / 产品,容易被判定为"在技术路线上走不长"。

10.9 薪资怎么谈

几个通用原则:

  1. 对方先问你期望薪资时,不要急着报数。 让对方先说区间,你再在这个区间里定位。
  2. 报数要有依据。 做过市场调研(同城市、同岗位、同经验区间的行情),或手里有真实 offer 作为锚点------而不是"我觉得"。
  3. 手上有 offer 才谈得硬气。 有一份保底 offer 在手,谈判时更从容,对方也能感觉到你不是随便投投。
  4. 别为了小差距反复折腾。 如果两家差一两千,跳过去要重新适应、重新证明自己,通常不划算;差距不明显就不值得动。
  5. 不要过早问加班、加班费等敏感问题。 时机很重要------在还没确定给你发 offer 之前问,容易被解读成"你对这份工作有意见" 。等进入谈 offer / 签三方阶段再问,换个不冒犯的问法:"团队平时的工作节奏大概是怎样的?"

10.10 自我介绍和反问,是唯二能自己掌控的环节

自我介绍 只有一两分钟,但这是唯一一段完全由你控制的时间 。不要复述简历,简历上有的人家自己会看。要讲简历上"不好写、但值得说"的东西:

  • 学校期间做过的一些小系统、小工具(简历放不下,但讲了能证明你在动手)
  • 一些能体现动手量的细节,比如"把某本经典书里的数据结构和算法全部手敲了一遍"
  • 技术栈的整体感:让他觉得"这个人从大学开始一直在学"

反问环节:不管面试官问不问你"还有什么想问的",都可以主动问。

一个几乎不会出错的问题:

"如果我想往 XX 方向(对方岗位的方向)发展,您觉得我还应该重点补哪些能力?"

好处是:既显得你有明确的职业意图,又能把面试官的话变成你下一轮复习的清单------他说的内容不重要,重要的是他愿意说。


结语

这套题整理下来,最大的感受是:面试考的不是"你会不会",而是"你能不能在压力下把会的东西准确地说出来"。

所以复习的终点不是"看完",而是"能出声讲一遍 "。一个便宜又有效的办法:把这份清单当成题库,找个人互相问------你问他、他用自己的话讲,你一听就知道"他这么讲合适吗",反过来自己讲的时候也能立刻发现自己卡在哪。

希望这份整理对准备 C++ 后端方向的你有用。文中有任何说得不准的地方,欢迎在评论区指正。

相关推荐
xlq223221 小时前
全面复习1
c++
Dylan的码园1 小时前
求平均成绩 矩阵数据处理 【循环处理 二维数组】
c++·线性代数·矩阵
sunshine22 girl1 小时前
Java学习五 面向对象高级5 内部类3-静态内部类和局部内部类(了解)
java·学习
en.en..1 小时前
Linux 进程退出exit(函数)
java·开发语言·算法
李游Leo1 小时前
HarmonyOS 7 TaskPool + Node-API + zstd:Native 批量压缩任务的主线程隔离、进度回传与资源收口【鸿蒙心迹】
java·华为·harmonyos
Light Gao1 小时前
Encoder 与 Decoder 详解:从 Embedding 到 Seq2Seq 的完整数据流
java·开发语言·embedding
All for pursuit.1 小时前
【回溯-2】22.括号生成
数据结构·c++·算法·leetcode
程序猿乐锅2 小时前
【黑马点评 | 第十一篇】关注 Feed 流实现
java·数据库·redis·分布式·后端·缓存·maven
longlongzihan2 小时前
LeetCode 1004.最大连续1的个数III 滑动窗口及动态规划题解
c++·算法·leetcode·dp·滑动窗口