C++ 大厂面经汇总:从多场技术面看筛选主线与高频题
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
面试官从简历直接追问
- shared_ptr 控制块里到底有什么?
- 虚表什么时候生成?对象里因此多了什么?
- vector 扩容后迭代器为什么会失效?
- 死锁你怎么现场定位?
- 为什么不先用 unique_ptr?
- 循环引用怎么发现?
- 跨线程怎么传所有权?
语言机制 / 对象模型
- 静态多态和动态多态分别怎么实现?代价是什么?
- 虚函数表和虚表指针分别在什么时候确定?放在哪?
- 为什么构造函数里调用虚函数通常得不到「派生类版本」?
- 为什么基类析构函数常常需要是虚函数?
- 构造函数可以是虚函数吗?纯虚函数可以有实现吗?
- 单继承和多继承下,对象内存布局有何差异?
- dynamic_cast 成功/失败的条件是什么?开销在哪?
- 成员初始化顺序由什么决定?和声明顺序不一致会怎样?
资源管理 / 现代 C++
- RAII 解决什么问题?和异常安全有什么关系?
- unique_ptr 和 shared_ptr 的所有权模型差在哪?
- shared_ptr 控制块包含什么?引用计数线程安全吗?
- 为什么优先 make_shared?它不能覆盖哪些场景?
- weak_ptr 如何打破循环引用?如何安全升级为 shared_ptr?
- enable_shared_from_this 解决什么问题?误用会怎样?
- 左值、右值、将亡值怎么区分?std::move 做了什么?
- 什么时候移动会被静默退化成拷贝?
- 手写一个简易 string,拷贝/移动/析构如何保证正确?
《参考解析》
这场面试的分水岭在哪。 面经总结出来的五条筛选主线很稳定:语言机制能不能落到内存图上(虚函数、对象布局、构造析构顺序、移动语义不是背定义);资源与生命周期能不能闭环(RAII、智能指针选型、循环引用、异常安全);STL 能不能讲取舍(底层结构、复杂度、失效规则、为什么选这个不选那个);并发与 OS 能不能托住业务(锁、条件变量、死锁、虚拟内存、epoll,权重因公司而异);项目能不能扛十五分钟追问。不同公司侧重点不同——OPPO、vivo 更吃基础加 OS 加工程表达,腾讯吃底层与网络,字节吃算法与现代 C++,小米吃项目闭环与性能稳定性——但主线填空了,侧重点再对也没用。
对象模型要能画图,不能停在「会写虚函数」。 每个含虚函数的对象里都有一个 vptr,指向该类的虚函数表;vptr 在构造函数里被设置,所以构造基类子对象时 vptr 指向基类的表,此时调用虚函数(直接调用或经非虚路径)拿到的都是基类版本——这正是「构造函数里不要调虚函数」的原因,同理析构期间虚函数也会退化。vtable 本身通常在只读数据段,vptr 在对象头部(多继承下可能有多个 vptr,每个基类子对象一张表,这也是 dynamic_cast 与指针调整开销的来源)。构造函数不能是虚函数,因为虚调用依赖已存在的 vptr,而 vptr 要等构造时才建立;纯虚函数可以有实现,派生类可以用 Base::f() 显式调用。基类析构必须是虚的,否则通过基类指针 delete 派生对象只析构基类部分,资源泄漏且是未定义行为。成员初始化顺序只由声明顺序决定,与初始化列表书写顺序无关,跨成员依赖写错会读到未初始化值。dynamic_cast 对多态类型走 RTTI 比对,指针失败返回 nullptr、引用失败抛 bad_cast,开销在运行时类型查找与指针偏移修正上,频繁调用应改为设计层面的替代方案(访问者模式、类型标签)。
智能指针与所有权是「我项目里全用 shared_ptr」之后必然被追的一层。 shared_ptr 的控制块里放着强引用计数、弱引用计数、删除器、分配器(以及 make_shared 时的对象本身),计数是原子的,所以「计数线程安全」成立;但被指向对象的读写完全没有保护,两个线程同时改同一个对象仍然要自己加锁——这句区分必须说清楚。默认优先 unique_ptr,因为它零额外开销、所有权唯一、能明确表达「谁负责释放」,之后按需转成 shared_ptr。循环引用破不掉是因为两个对象的强计数互相撑着,谁都到不了零;发现方式是观察对象析构日志、用 weak_ptr 打断其中一条边,或者用工具看引用计数不归零。weak_ptr 不增加强计数,升级要用 lock() 并判空(不能先 expired 再 lock,那是竞态)。make_shared 一次分配搞定控制块与对象、异常安全更好、缓存局部性更好,但它不能覆盖需要自定义删除器、需要从 weak_ptr 在对象销毁后仍能访问控制块的场景,另外对象与控制块同生共死会推迟内存归还。enable_shared_from_this 让成员函数内部拿到自己的 shared_ptr,误用是在栈对象或已被 unique_ptr 持有的对象上调用,会对控制块做出错误假设而重复析构。
现代 C++ 的区分度不在特性清单,而在能不能对应到一次取舍。 移动构造与移动赋值解决的是「临时对象也要深拷贝」的成本;编译器在返回值优化、按值返回局部变量、传右值这些位置会自动移动,但一旦走了 const 引用、或者写了 return std::move(x) 反而会阻断 RVO,这就是「你以为在移动其实还在拷贝」的典型来源。std::move 本身不移动,只是把表达式转成右值引用,真正的动作发生在被绑定的移动构造/赋值里。emplace 系列就地构造,省掉一次临时对象与移动,收益边界在「参数是构造实参而非已有对象」时最明显。STL 侧要能把失效规则讲准:vector 扩容导致所有迭代器、指针、引用失效,中间插入删除影响插入点之后;deque 首尾插入失效迭代器但引用通常保留;list/map/set 只在被删节点上失效;unordered_map 在 rehash 时全部迭代器失效。选型看需求:map 是有序、稳定 O(log n)、适合范围查询;unordered_map 期望 O(1)、最坏 O(n),要防哈希冲突攻击就得用随机种子或自定义哈希,负载因子与 rehash 成本要算进去。
并发与 OS 是 OPPO、vivo 这类系统岗结合业务追问最多的部分。 「死锁怎么现场定位」的标准答案不是背四个必要条件,而是给方法:先看现象(进程 hung 住、CPU 低、无日志推进),再用 gdb 附着后 thread apply all bt 看每个线程卡在哪个锁上,顺带看锁的归属线程是谁,pstack、gcore、eBPF 的 offcputime 都能用来量化阻塞;定位到代码后归因到锁顺序不一致、锁粒度过大、或条件变量使用错误。条件变量必须配 while 循环等(防虚假唤醒与被抢跑),notify 时最好在持锁状态下改完条件再唤醒(或至少在谓词与通知之间保证顺序)。虚拟地址到物理页靠多级页表,缺页由 MMU 触发异常后内核补齐,问题答到这一层通常还会被问 TLB 与缺页对性能的影响。epoll 的水平触发与边缘触发要能讲清行为差异与配套写法(ET 必须循环读到 EAGAIN、必须用非阻塞 fd)。这些点只要被追到第二层就空,基本会被判定为「背过但没做过」。
项目深挖和准备顺序。 通过率高的人手里都有一个能讲十五分钟的项目:背景与指标(延迟、内存、稳定性、吞吐)、职责边界(不要把团队活说成个人活)、关键设计与取舍(为什么用这条数据通路、这种同步、这种缓存)、至少一个完整故障闭环(现象 → 假设 → 证据 → 修复 → 回归)、以及与目标岗位的迁移关系。时间有限的准备顺序建议是:先打穿 C++ 主线(对象模型、RAII 与智能指针、移动语义、STL 取舍),再补并发与 OS(锁、条件变量、死锁、虚拟内存、进程线程,冲腾讯再加网络),只深挖一个能扛追问的项目,最后按目标公司偏科——先背三百道题、项目还是糊的,是面经里被反复验证过的失败路径。