小米 C++ 软件开发 一面 面经
小米一面整体节奏很快,面试官技术功底扎实,问题层层递进,不会只停在概念层面,基本每道题都会追问"为什么"或者"你在项目里怎么用的"。整个面试大概 60 分钟,前半段是基础八股,后半段直接上手撕代码,压力不小但氛围还算友好。建议提前把 C++ 内存模型、并发、STL 底层这几块吃透,代码题要写得干净,边界条件别漏。
1. 自我介绍
略(根据个人情况准备,建议控制在 2 分钟内,突出项目亮点和技术栈)
2. 进程和线程的区别?进程间通信有哪些方式?线程间通信呢?
进程是资源分配的基本单位,线程是 CPU 调度的基本单位。同一进程内的线程共享堆、全局变量、文件描述符,但各自拥有独立的栈和寄存器。
进程间通信(IPC)方式:
- 管道(pipe):半双工,只能父子进程间用
- 命名管道(FIFO):可以无亲缘关系进程间通信
- 消息队列:内核维护的消息链表,支持按类型读取
- 共享内存:最快的 IPC 方式,需要配合信号量同步
- 信号量:用于进程/线程间同步
- Socket:可跨机器通信,最通用
线程间通信相对简单,共享内存天然可见,主要靠 mutex、condition_variable、atomic 来同步。
3. std::move 和 std::forward 的区别?什么场景下用 forward?
std::move 无条件将左值转换为右值引用,触发移动语义,不做任何转发逻辑。
std::forward 是完美转发,保留参数的值类别(左值还是右值)。典型场景是模板函数中:
template<typename T>
void wrapper(T&& arg) {
// 如果用 move,左值参数也会被当右值传,语义错误
target(std::forward<T>(arg)); // 正确:保留原始值类别
}
forward 依赖模板类型推导,单独使用没有意义,必须配合万能引用(T&&)。
4. std::shared_ptr 的实现原理?线程安全性如何?循环引用怎么解决?
shared_ptr 内部维护两个指针:一个指向托管对象,一个指向控制块(control block)。控制块里有引用计数(use_count)和弱引用计数(weak_count),用原子操作保证计数本身的线程安全。
但注意:引用计数的增减是线程安全的,但对同一个 shared_ptr 对象的并发读写不是线程安全的(比如两个线程同时对同一个 shared_ptr 变量赋值)。
循环引用:A 持有 B 的 shared_ptr,B 持有 A 的 shared_ptr,导致引用计数永远不为 0,内存泄漏。解决方案是将其中一方改为 weak_ptr,weak_ptr 不增加引用计数,使用时调用 lock() 获取临时 shared_ptr。
5. volatile 关键字的作用?它能保证线程安全吗?
volatile 告诉编译器不要对该变量做优化,每次读写都直接访问内存,防止编译器将其缓存在寄存器中。
典型场景:内存映射 IO、硬件寄存器访问、信号处理函数中修改的变量。
不能保证线程安全。volatile 只阻止编译器优化,不提供原子性,也不产生内存屏障。多线程场景应该用 std::atomic 或 mutex。
6. 什么是内存对齐?为什么需要内存对齐?如何手动控制结构体的内存对齐?
内存对齐是指数据存储的起始地址必须是其大小的整数倍。比如 int(4字节)的地址必须是 4 的倍数。
原因:
- CPU 访问对齐的数据只需一次总线操作,非对齐可能需要两次,性能差
- 某些架构(如 ARM)直接不支持非对齐访问,会触发硬件异常
struct A {
char a; // 1 byte + 3 padding
int b; // 4 bytes
char c; // 1 byte + 3 padding
};
// sizeof(A) = 12
#pragma pack(1) // 取消对齐,按1字节紧凑排列
struct B {
char a;
int b;
char c;
};
// sizeof(B) = 6
#pragma pack()
// 或者用 alignas
struct alignas(16) C { int x; };
7. 虚函数表是什么?多重继承下虚函数表结构是怎样的?
每个含虚函数的类都有一张虚函数表(vtable),存放虚函数指针数组。每个对象头部有一个 vptr 指向该表。调用虚函数时通过 vptr 找到 vtable,再找到对应函数指针,实现运行时多态。
多重继承下,派生类会有多个 vptr,每个基类对应一张 vtable。比如:
class A { virtual void f(); };
class B { virtual void g(); };
class C : public A, public B { void f() override; void g() override; };
C 的对象布局中有两个 vptr,分别对应 A 和 B 的 vtable。调用 ((B*)c)->g() 时会用第二个 vptr。这也是为什么多重继承下指针转换可能会调整地址偏移(thunk 机制)。
8. std::unordered_map 和 std::map 的底层实现及时间复杂度对比
底层结构 |
红黑树 |
哈希表(链地址法) |
查找 |
O(log n) |
平均 O(1),最坏 O(n) |
插入 |
O(log n) |
平均 O(1) |
有序性 |
有序 |
无序 |
内存 |
较少 |
较多(桶数组) |
unordered_map 最坏 O(n) 发生在哈希冲突严重时(比如所有 key 哈希到同一个桶)。自定义类型作 key 需要提供 hash 和 == 运算符。
9. std::vector 的扩容机制?扩容代价多大?如何避免频繁扩容?
vector 容量不足时,通常按 2 倍(GCC)或 1.5 倍(MSVC)扩容:分配新内存 → 移动/拷贝旧元素 → 释放旧内存。
扩容代价:O(n) 时间,所有迭代器、指针、引用全部失效。
避免频繁扩容:
- 提前
reserve(n)预分配容量 - 如果知道大概大小,构造时传入 size
- 用
emplace_back代替push_back减少拷贝
10. 什么是虚假共享(False Sharing)?如何避免?
多个线程访问不同变量,但这些变量恰好在同一个 CPU 缓存行(通常 64 字节)内,导致一个线程修改自己的变量时,另一个线程的缓存行也被
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。
