网易 C++ 研发岗 一面 高强度面试

1. C++ 中 move 语义和完美转发的区别是什么?

答:move 语义通过 std::move 将左值强制转为右值引用,触发移动构造/移动赋值,避免深拷贝,转移资源所有权。转移后原对象处于"有效但未定义"状态,不能再使用其资源。

完美转发通过 std::forward 配合模板的万能引用(T&&),在函数模板中将参数以原始的值类别(左值/右值)转发给下一个函数,避免多次拷贝。

核心区别:move 是"无条件转为右值",forward 是"保持原来的值类别转发"。

template<typename T>
void wrapper(T&& arg) {
    target(std::forward<T>(arg)); // 完美转发,保留左值/右值属性
}

2. 虚函数表(vtable)的结构是什么?多继承时虚函数表怎么布局?

答:每个含虚函数的类有一张 vtable,存放该类所有虚函数的函数指针。每个对象的内存开头有一个 vptr 指向这张表。

单继承时:子类 vtable 在父类基础上覆盖被重写的函数指针,新增虚函数追加在末尾。

多继承时:对象内存中有多个 vptr,每个基类对应一张 vtable。子类对象的内存布局大致如下:

[vptr1 → Base1的vtable]  [Base1的成员]
[vptr2 → Base2的vtable]  [Base2的成员]
[Derived自己的成员]

调用不同基类的虚函数时,this 指针会做偏移调整(thunk 机制),指向对应基类子对象。

3. std::shared_ptr 的引用计数是线程安全的吗?指向的对象呢?

答:引用计数本身是线程安全的,内部用原子操作(std::atomic)维护计数,多线程同时拷贝/销毁 shared_ptr 不会导致计数错误。

但 shared_ptr 对象本身不是线程安全的:多线程同时对同一个 shared_ptr 实例进行读写(比如一个线程在 reset,另一个在拷贝),会有数据竞争,需要加锁。

shared_ptr 指向的对象完全不在 shared_ptr 的保护范围内,线程安全性取决于对象自身的实现。

总结:

  • 引用计数:线程安全
  • shared_ptr 实例的并发读写:不安全
  • 所指对象的并发访问:不安全

4. 内存泄漏的常见原因有哪些?如何排查?

答:常见原因:

  • new 了对象但没有对应的 delete
  • 异常抛出导致 delete 没有执行
  • 循环引用(shared_ptr 互相持有)
  • 容器中存放裸指针,容器清空时没有释放指针指向的内存
  • 回调/闭包中捕获了 shared_ptr 形成环

排查方法:

  • Valgrind(Linux):valgrind --leak-check=full ./程序,能精确定位泄漏位置
  • AddressSanitizer:编译时加 -fsanitize=address,运行时报告
  • Visual Studio 的 CRT 调试堆:_CrtDumpMemoryLeaks()
  • 代码层面:用 RAII、智能指针替代裸指针,避免手动 new/delete

5. std::map 和 std::unordered_map 底层结构分别是什么?如何选择?

答:

  • std::map:红黑树(自平衡BST),有序,查找/插入/删除均为 O(log n)
  • std::unordered_map:哈希表(开链法),无序,平均 O(1),最坏 O(n)(哈希冲突严重时)

选择依据:

  • 需要有序遍历、范围查询(lower_bound/upper_bound)→ 用 map
  • 只需要快速查找,不关心顺序 → 用 unordered_map
  • key 是自定义类型且难以写哈希函数 → 用 map(只需实现 < 运算符)
  • 数据量大、性能敏感 → 优先 unordered_map,但注意哈希冲突和内存碎片

6. 解释一下 C++ 的 RAII 机制,它解决了什么问题?

答:RAII(Resource Acquisition Is Initialization):资源的获取在构造函数中完成,资源的释放在析构函数中完成。对象生命周期结束时,析构函数自动调用,资源自动释放。

解决的问题:

  • 避免资源泄漏:即使发生异常,栈上对象的析构函数也会被调用
  • 避免手动管理资源的遗漏(忘记 delete、忘记 close)
  • 代码更简洁,资源管理逻辑集中在类内部

典型应用:

  • std::unique_ptr / std::shared_ptr:管理堆内存
  • std::lock_guard / std::unique_lock:管理互斥锁
  • std::fstream:管理文件句柄

7. 进程和线程的区别?什么时候用多进程,什么时候用多线程?

答:

  • 进程是资源分配的基本单位,有独立的地址空间;线程是调度的基本单位,共享进程的地址空间
  • 进程间通信(IPC)需要管道、socket、共享内存等机制,开销大;线程间直接共享内存,通信方便但需要同步
  • 进程崩溃不影响其他进程;一个线程崩溃可能导致整个进程崩溃
  • 创建/切换进程开销远大于线程

选择:

  • 多进程:需要强隔离(如浏览器每个Tab一个进程)、子任务可能崩溃、需要利用多核且任务相对独立
  • 多线程:任务间需要频繁共享数据、对延迟敏感、任务粒度细(如游戏服务器的逻辑线程+网络线程+渲染线程)

8. TCP 的 TIME_WAIT 状态为什么需要等待 2MSL?大量 TIME_WAIT 怎么处理?

答:TIME_WAIT 出现在主动关闭连接的一方,等待时间为 2MSL(Maximum Segment Lifetime,通常60秒)。

原因:

  • 确保最后一个 ACK 能到达对端:如果最后的 ACK 丢失,对端会重发 FIN,本端需要还在 TIME_WAIT 状态才能重新发 ACK
  • 让网络中残留的旧数据包消亡:防止旧连接的延迟数据包被新连接误收

大量 TIME_WAIT 的处理:

  • 开启 SO_REUSEADDR:允许端口复用
  • 开启 tcp_tw_reuse(Linux):允许将 TIME_WAIT 的连接复用给新连接(需要时间戳支持)
  • 调小 tcp_fin_timeout:缩短等待时间
  • 架构层面:使用连接池,减少频繁建立/断开连接;服务端尽量让客户端主动关闭连接

9. 什么是内存屏障(Memory Barrier)?为什么需要它?

答:内存屏障是一种 CPU 指令,用于阻止编译器和 CPU 对内存操作进行重排序。

为什么需要:

  • 现代 CPU 为了性能会乱序执行指令,多核环境下每个核有自己的缓存,写操作不一定立即对其他核可见
  • 在多线程编程中,如果没有内存屏障,线程A写入的数据,线程B可能看不到最新值,或者看到的顺序和写入顺序不一致

C++ 中的体现:

  • std::atomic 的各种内存序(memory_order_acquirememory_order_releasememory_order_seq_cst)本质上就是在控制内存屏障的强度
  • acquire:屏障之后的读写不能重排到屏障之前
  • release:屏障之前的读写不能重排到屏障之后
  • seq_cst:最强,全局顺序一致,性能最低

10. 解释一下 C++ 模板的特化和偏特化,有什么实际用途?

答:全特化:为模板的某一组具体类型提供完全不同的实现。偏特化:只固定部分模板参数,或对参数加约束(如指针类型),提供特殊实现。

// 主模板
template<typename T> struct Traits { ... };

// 全特化:T = int
template<> st

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

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

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

全部评论

相关推荐

评论
1
2
分享

创作者周榜

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