网易 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_acquire、memory_order_release、memory_order_seq_cst)本质上就是在控制内存屏障的强度acquire:屏障之后的读写不能重排到屏障之前release:屏障之前的读写不能重排到屏障之后seq_cst:最强,全局顺序一致,性能最低
10. 解释一下 C++ 模板的特化和偏特化,有什么实际用途?
答:全特化:为模板的某一组具体类型提供完全不同的实现。偏特化:只固定部分模板参数,或对参数加约束(如指针类型),提供特殊实现。
// 主模板
template<typename T> struct Traits { ... };
// 全特化:T = int
template<> st
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

