小米 C++ 二面 攒人品了
二面明显比一面难度上了一个台阶,面试官是组内 senior,开场直接问项目深挖,每个技术决策都要说清楚为什么这么做、有没有更好的方案。八股比例少了,更多是系统设计和场景题,考察你有没有真正在工程里踩过坑。代码题也更偏综合,不是纯算法,而是结合实际场景。整体感受是:说不知道比瞎说好,面试官很容易听出来你是背的还是真懂的。
1. 自我介绍 + 项目深挖
略(重点准备:项目中遇到的性能瓶颈、你做了什么优化、结果如何量化)
2. C++ 内存模型中的 memory order 有哪些?memory_order_relaxed 和 memory_order_seq_cst 的区别?
C++11 定义了六种内存序:
relaxed |
只保证原子性,不保证顺序 |
consume |
依赖链上的读操作有序(实践中很少用) |
acquire |
当前线程后续读写不会重排到此操作之前 |
release |
当前线程之前的读写不会重排到此操作之后 |
acq_rel |
acquire + release |
seq_cst |
全局顺序一致,最强,也最慢 |
std::atomic<int> x{0};
// relaxed:只保证 x 的读写是原子的,不管顺序
x.store(1, std::memory_order_relaxed);
// seq_cst:所有线程看到的操作顺序一致,有全局内存屏障
x.store(1, std::memory_order_seq_cst);
典型用法:生产者用 release 写数据,消费者用 acquire 读,配对使用保证可见性,比 seq_cst 性能好。
3. 说说 C++ 对象的生命周期和 RAII,你在项目里怎么用的?
RAII(Resource Acquisition Is Initialization):资源在构造时获取,在析构时释放,利用栈对象生命周期自动管理资源,避免泄漏。
标准库里的 RAII 例子:unique_ptr、lock_guard、fstream、vector。
自定义 RAII:
class FileHandle {
FILE* fp_;
public:
explicit FileHandle(const char* path) {
fp_ = fopen(path, "r");
if (!fp_) throw std::runtime_error("open failed");
}
~FileHandle() { if (fp_) fclose(fp_); }
// 禁止拷贝,允许移动
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& o) noexcept : fp_(o.fp_) { o.fp_ = nullptr; }
};
关键点:析构函数不能抛异常(noexcept),移动时要把源对象置空,防止 double free。
4. 模板元编程中 std::enable_if 和 if constexpr 的区别?各自适用场景?
std::enable_if 基于 SFINAE,在编译期根据条件启用/禁用函数重载:
template<typename T>
std::enable_if_t<std::is_integral_v<T>, void>
process(T val) { /* 整数版本 */ }
if constexpr(C++17)在函数体内做编译期分支,更直观:
template<typename T>
void process(T val) {
if constexpr (std::is_integral_v<T>) {
// 整数分支,另一分支不参与编译
} else {
// 其他类型分支
}
}
区别:enable_if 用于控制重载集合,if constexpr 用于函数体内分支。C++17 后优先用 if constexpr,代码更清晰。
5. 什么是协程?C++20 的协程和线程有什么本质区别?
协程是可以暂停和恢复执行的函数,暂停时不阻塞线程,控制权交回调用方。
与线程的本质区别:
- 线程是 OS 调度的,切换有内核开销(上下文切换约 1~10μs)
- 协程是用户态调度的,切换只是函数调用级别的开销(纳秒级)
- 线程是抢占式的,协程是协作式的(主动 co_await 让出)
- 一个线程可以跑成千上万个协程
C++20 协程三个关键字:co_await、co_yield、co_return。适合 IO 密集型场景,比如网络服务器,避免为每个连接创建一个线程。
6. 说说 std::deque 的底层结构,为什么它的迭代器比 vector 复杂?
deque 底层是分段连续内存:维护一个指针数组(map),每个指针指向一块固定大小的内存块(chunk)。
map: [ptr0] [ptr1] [ptr2] ...
| | |
chunk0 chunk1 chunk2
这样头尾插入都是 O(1),不需要整体搬移。
迭代器复杂的原因:迭代器需要同时维护当前元素指针、当前 chunk 的首尾边界、以及 map 中的位置,++ 操作需要判断是否跨 chunk。而 vector 迭代器就是一个裸指针,++ 直接加一。
7. 一个 C++ 程序从源码到运行,经历了哪些阶段?动态链接和静态链接的区别?
编译流程:
- 预处理(cpp):展开宏、include,生成
.i - 编译(cc1):生成汇编
.s - 汇编(as):生成目标文件
.o - 链接(ld):合并
.o,解析符号,生成可执行文件
静态链接:库代码直接打包进可执行文件,体积大,无运行时依赖,部署简单。
动态链接:可执行文件只记录库名和符号,运行时由动态链接器(ld.so)加载 .so/.dll。多个进程共享同一份库的内存映射,节省内存,但有版本依赖问题(DLL hell)。
8. 说说 Linux 的虚拟内存布局,栈和堆的增长方向?mmap 映射在哪里?
典型 64 位 Linux 进程地址空间(从低到高):
0x0000... 保留(NULL 陷阱)
代码段(text)
数据段(data/bss)
堆(向高地址增长)↑
...
mmap 区域(共享库、匿名映射)
...
栈(向低地址增长)↓
0xFFFF... 内核空间
- 堆:
brk/sbrk或mmap分配,向高地址增长 - 栈:向低地址增长,默认大小通常 8MB(
ulimit)
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

查看13道真题和解析