网易 C++ 研发岗 二面 面经

1. C++ 的内存模型中,happens-before 关系是什么?

答:happens-before 是 C++11 内存模型中定义的一种偏序关系,用来描述操作间的可见性保证:如果操作 A happens-before 操作 B,那么 A 的结果对 B 一定可见。

建立 happens-before 的方式:

  • 同一线程内,代码顺序靠前的操作 happens-before 靠后的操作(sequenced-before)
  • mutex 的 unlock happens-before 下一次 lock
  • atomic 的 release 写 happens-before 另一个线程对同一变量的 acquire 读
  • thread::join() 使被等待线程的所有操作 happens-before join 返回后的操作

没有 happens-before 关系的并发读写就是数据竞争(data race),属于未定义行为,编译器和 CPU 可以做任意优化。

2. std::unique_ptr 的实现原理是什么?为什么它的大小通常和裸指针一样?

答:unique_ptr 内部持有一个裸指针和一个删除器(deleter)。析构时调用删除器释放资源,禁用拷贝构造和拷贝赋值,只允许移动。

大小和裸指针相同的原因:

  • 默认删除器 std::default_delete<T> 是一个空类,编译器对其做了空基类优化(EBO)
  • unique_ptr 内部通常用 std::tuple 或直接继承删除器来存储,空删除器不占额外空间
  • 所以 sizeof(unique_ptr<T>) == sizeof(T*) == 8(64位)

如果使用自定义有状态的删除器(比如 lambda 捕获了变量),则 unique_ptr 的大小会增加。

3. 编译器是如何实现虚函数调用的?虚函数调用比普通函数调用慢在哪里?

答:虚函数调用的汇编过程:

  1. 从对象内存起始位置取出 vptr
  2. 通过 vptr 找到 vtable
  3. 根据虚函数的固定偏移量从 vtable 中取出函数指针
  4. 通过函数指针间接调用

比普通函数调用慢的原因:

  • 多了两次内存间接寻址(取 vptr、取函数指针),普通函数调用地址在编译期确定,直接 call
  • 间接调用导致 CPU 分支预测失效,流水线可能被打断
  • vptr 和 vtable 的访问可能造成 cache miss,尤其是多态容器中对象类型多样时

实际影响:单次调用差距在纳秒级,性能敏感的热路径(如游戏每帧调用百万次)才需要考虑,可以用 CRTP(奇异递归模板模式)实现编译期多态来规避。

4. 什么是 ABA 问题?如何解决?

答:ABA 问题出现在无锁编程的 CAS(Compare-And-Swap)操作中:

  • 线程1读取变量值为 A,准备 CAS
  • 线程2将值从 A 改为 B,再改回 A
  • 线程1执行 CAS,发现值还是 A,认为没有变化,操作成功

但实际上值已经被修改过了,在某些场景下(如链表节点被删除后重新分配到同一地址)会导致逻辑错误。

解决方案:

  • 带版本号的 CAS(Tagged Pointer):将指针和版本号打包在一起,每次修改版本号递增,即使值相同版本号也不同。C++ 中可以用 std::atomic<std::pair<T*, int>> 或利用指针高位存版本号
  • std::atomiccompare_exchange 配合版本号
  • 使用 Hazard Pointer 或 RCU(Read-Copy-Update)等无锁内存回收机制,从根本上避免地址复用

5. 解释一下 C++ 的模板元编程,举一个实际用途。

答:模板元编程(TMP)是利用 C++ 模板在编译期进行计算的技术,本质上是图灵完备的。编译期计算的结果直接嵌入生成的代码,零运行时开销。

核心机制:模板特化作为"if-else",递归实例化作为"循环"。

实际用途——编译期类型检查和条件分发:

// 编译期判断类型,选择不同实现
template<typename T>
void serialize(T val) {
    if constexpr (std::is_integral_v<T>) {
        // 整数序列化
    } else if constexpr (std::is_floating_point_v<T>) {
        // 浮点序列化
    }
}

另一个典型用途是 std::tuple 的实现、std::variant 的访问、以及各种 type traits。在游戏引擎中常用 TMP 实现零开销的组件系统(ECS)。

6. 进程间通信有哪些方式?共享内存为什么是最快的?

答:主要方式:

  • 管道(pipe):单向,只能父子进程间使用;命名管道(FIFO)可以无关进程使用
  • 消息队列:内核维护的消息链表,支持按类型读取,有大小限制
  • 共享内存:多个进程映射同一块物理内存,直接读写
  • 信号量:用于进程间同步,通常配合共享内存使用
  • Socket:支持跨机器通信,最通用但开销最大
  • 信号(signal):只能传递信号编号,信息量极少

共享内存最快的原因:

  • 其他 IPC 方式(管道、消息队列、socket)都需要将数据从用户空间拷贝到内核空间,再从内核空间拷贝到另一个进程的用户空间,至少两次拷贝
  • 共享内存直接映射到两个进程的虚拟地址空间,读写不需要系统调用,也没有数据拷贝,只有一次内存访问

代价:需要自己实现同步(用信号量或互斥锁),否则有竞争条件。

7. 说一下 Linux 的虚拟内存机制,一个进程的地址空间是如何布局的?

答:虚拟内存让每个进程以为自己独占整个地址空间,通过页表将虚拟地址映射到物理地址,由 MMU 硬件完成转换。

64位 Linux 进程地址空间布局(从低到高):

0x0000000000000000  ← NULL(不可访问)
        ↓
    .text段          ← 代码段(只读)
    .rodata段        ← 只读数据(字符串常量等)
    .data段          ← 已初始化全局/静态变量
    .bss段           ← 未初始化全局/静态变量(不占文件空间)
        ↓
    堆(向上增长)    ← malloc/new
        ↓
    内存映射区        ← mmap、动态库、共享内存
        ↓
    栈(向下增长)    ← 函数调用栈
    内核空间          ← 用户态不可直接访问

页表缺页中断:访问虚拟地址时,如果页表项不存在(页不在物理内存中),触发缺页中断,内核将对应页从磁盘换入内存,更新页表,再重新执行指令。

8. 什么是 false sharing?如何避免?

答:False sharing(伪共享)是多核 CPU 缓存一致性导致的性能问题:

CPU 缓存的最小单位是缓存行(Cache Line),通常64字节。如果两个线程分别操作不同的变量,但这两个变量恰好在同一个缓存行内,那么一个线程修改自己的变量时,会导致另一个线程的缓存行失效,另一个线程必须重新从内存加载,即使它根本没有访问被修改的变量。

// 有 false sharing 的问题
struct Counter {
    int a; // 线程1使用
    int b; // 线程2使用
    // a 和 b 在同一缓存行,互相干扰
};

// 解决:填充到缓存行大小
struct Counter {
    alignas(64) int a;
    alignas(64) int b;
};

避免方法:

  • alignas(64) 将不同线程访问的变量对齐到不同缓存行
  • 将线程私有数据放在线程本地存储(thread_local
  • 设计数据结构时,将同一线程频繁访问的数据放在一起(数据局部性)

9. 解释一下 B+ 树,为什么数据库索引用 B+ 树而不用红黑树?

答:B+ 树是一种多路平衡搜索树,所有数据都存在叶子节点,叶子节点之间用链表相连,内部节点只存键值用于导航。

为什么数据库用 B+ 树而不用红黑树:

磁盘 IO 是瓶颈:数据库索引存在磁盘上,每次读取一个节点就是一次磁盘 IO,耗时约10ms。红黑树每个节点只存一个键值,树高为 O(log₂ n),1亿条数据树高约27层,需要27次 IO。B+ 树每个节点可以存几百个键值(一个磁盘页4KB),树高只有3-4层,IO 次数极少。

B+ 树 vs B 树:B+ 树内部节点不存数据,可以存更多键值,树更矮;叶子节点链表支持高效范围查询(BETWEENORDER BY),B 树做范围查询需要中序遍历,效率低。

红黑树适合内存中的有序数据结构(如 std::map),不适合磁盘场景。

10. 手撕题:实现一个 LRU 缓存,要求 get 和 put 都是 O(1)。

答:哈希表 + 双向链表:哈希表实现 O(1) 查找,双向链表维护访问顺序(最近访问的在头部,最久未访问的在尾部)。

class LRUCache {
    int cap;
    list<pair<int,int>> lst; // {key, val}
    unordered_map<int, list<pair<int,int>>::

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++八股文全集 文章被收录于专栏

本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

全部评论

相关推荐

乐观向上的码农:HR面别挂我,HR面别挂我,HR面别挂我,HR面别挂我,HR面别挂我,HR面别挂我,HR面别挂我,HR面别挂我,HR面别挂我,HR面别挂我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,别排序我,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,抖音不如快手一根,
点赞 评论 收藏
分享
牛客62533758...:华为不卡双非,而是卡院校hhhh
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务