金山 C++ 一面 面经
1. 自我介绍
简单做一下自我介绍,重点介绍自己的技术栈、做过的项目、项目中承担的职责,以及和 C++ 岗位相关的能力即可。
回答时不需要铺得太开,控制在 1 分钟左右比较合适。
参考回答:
面试官您好,我是 XX,目前主要学习和使用的是 C++,系统学过 C++ 基础、数据结构、操作系统、计算机网络和数据库等内容。项目方面,我做过一个基于 C++ 的高并发服务端项目,也接触过多线程、网络编程、线程同步、内存管理等内容。在项目里我主要负责模块设计、接口实现和问题排查。今天也希望通过这次面试,和您交流一下我对 C++ 岗位相关知识的理解。
2. 讲一下你做过的一个 C++ 项目,重点说你负责的部分
参考回答:
回答这个问题时,不要从头到尾流水账介绍项目,而是建议按“项目背景 - 技术选型 - 个人职责 - 难点 - 收获”来讲。
例如可以这样回答:
我做过一个基于 C++ 的网络服务端项目,项目主要目标是实现多客户端连接、消息收发和业务处理。技术上主要用了 C++11、多线程、socket 编程、互斥锁、条件变量等。
我主要负责的是客户端连接管理、任务分发和部分业务逻辑模块的开发。
在实现过程中,比较大的难点是多个线程同时访问共享队列时容易出现竞争问题,所以我引入了互斥锁和条件变量来保证线程安全,同时减少线程空转带来的性能损耗。
通过这个项目,我对 C++ 在工程中的使用、多线程同步、资源管理等有了更深入的理解。
这个问题的核心不是项目有多大,而是你能不能把自己的工作讲清楚,尤其要能讲出“你做了什么”“遇到了什么问题”“怎么解决的”。
3. 进程和线程有什么区别
参考回答:
进程和线程都是操作系统进行资源管理和任务调度的重要概念,但两者有明显区别。
第一,进程是资源分配的基本单位。
每个进程都有自己独立的地址空间、代码段、数据段和打开的文件等资源。不同进程之间默认相互隔离,一个进程崩溃一般不会直接影响另一个进程。
第二,线程是 CPU 调度的基本单位。
线程是进程中的执行单元,同一个进程内的多个线程共享进程的地址空间、全局变量、堆、文件描述符等资源,但每个线程有自己的栈、寄存器上下文和程序计数器。
第三,创建和切换开销不同。
进程创建和销毁的开销通常更大,线程相对更轻量;线程间切换的代价一般也比进程间切换更小。
第四,通信方式不同。
进程之间因为资源隔离,需要借助管道、消息队列、共享内存、socket 等 IPC 方式进行通信。线程由于共享进程内存,可以直接读写共享数据,但也因此更容易出现线程安全问题。
总结来说,进程更强调资源隔离,线程更强调执行并发。
4. 线程同步常见方式有哪些
参考回答:
线程同步的核心目的是协调多个线程对共享资源的访问,避免竞态条件、数据不一致等问题。常见方式主要有以下几种。
第一,互斥锁。
互斥锁用于保护临界区,保证同一时刻只有一个线程能访问共享资源。它适合对共享变量、共享容器等进行保护。
缺点是如果加锁范围过大,可能会影响并发性能;如果使用不当,还可能导致死锁。
第二,读写锁。
读写锁区分读操作和写操作。当多个线程都只是读取数据时,可以同时加读锁;只有写操作时才需要独占。
这种方式适合“读多写少”的场景。
第三,条件变量。
条件变量通常和互斥锁一起使用,用于线程之间的等待和通知。例如生产者消费者模型中,当队列为空时消费者等待,当生产者放入数据后通知消费者。
它可以避免线程不断轮询带来的 CPU 浪费。
第四,原子操作。
对于简单的共享变量操作,比如计数器加减,可以直接使用原子类型。原子操作底层通常借助 CPU 指令实现,效率高于传统加锁。
但原子操作更适合简单场景,复杂逻辑一般还是要配合锁来做。
第五,信号量。
信号量本质上是一个计数器,用于控制多个线程对有限资源的访问数量。例如一个资源池最多允许 N 个线程同时使用。
实际开发中,应该根据共享资源的复杂度和性能要求选择合适的同步机制,而不是一律使用互斥锁。
5. 什么是死锁,死锁产生的条件是什么
参考回答:
死锁是指多个线程或进程在执行过程中,因为争夺资源而相互等待,最终谁都无法继续执行的状态。
死锁产生通常需要同时满足以下四个条件。
第一,互斥条件。
资源在某一时刻只能被一个线程占用。
第二,请求并保持条件。
线程已经持有一个资源,同时又请求新的资源,并且在等待新资源时不释放已有资源。
第三,不可剥夺条件。
线程已经获得的资源在没有使用完之前,不能被其他线程强行夺走。
第四,循环等待条件。
多个线程之间形成一个资源等待环。比如线程 A 等待线程 B 手里的资源,线程 B 又等待线程 A 手里的资源。
举个简单例子:
线程 A 先拿锁 1 再等锁 2,线程 B 先拿锁 2 再等锁 1,这时就可能产生死锁。
避免死锁的常见方法有:
- 固定加锁顺序,所有线程按相同顺序申请锁。
- 尽量减少锁的嵌套。
- 使用
std::lock或一次性获取多个锁。 - 设置超时机制,避免无限等待。
- 优化设计,减少共享资源竞争。
6. std::vector 和 std::list 有什么区别
参考回答:
std::vector 和 std::list 都是 STL 容器,但底层结构和适用场景差别很大。
std::vector 底层是连续内存空间,类似动态数组。
优点是:
- 支持随机访问,
vector[i]的时间复杂度是 O(1)。 - 缓存友好,遍历效率高。
- 尾部插入和删除效率较高,均摊复杂度通常是 O(1)。
缺点是:
- 中间插入和删除效率低,因为可能需要移动大量元素。
- 扩容时可能发生整块内存重新分配,导致迭代器失效。
std::list 底层通常是双向链表。
优点是:
- 任意位置插入和删除效率高,在已知位置的情况下时间复杂度通常是 O(1)。
- 插入删除时不会像
vector那样大规模搬移元素。 - 节点地址相对稳定,已有迭代器通常不容易整体失效。
缺点是:
- 不支持随机访问。
- 每个节点需要额外存储前后指针,内存开销更大。
- 节点分散,缓存局部性差,遍历效率往往不如
vector。
如果业务主要是大量遍历、随机访问、尾插,优先考虑 vector。
如果业务是频繁在中间位置插入删除,而且已经持有对应迭代器,list 才更有优势。
7. map 和 unordered_map 的区别是什么
参考回答:
二者都是键值对容器,但底层实现和使用特点不同。
map 底层通常基于红黑树实现,特点是:
- 元素默认按 key 有序。
- 查找、插入、删除的时间复杂度通常是 O(logn)。
- 支持范围查找、有序遍历等操作。
unordered_map 底层通常基于哈希表实现,特点是:
- 元素无序。
- 平均情况下查找、插入、删除复杂度是 O(1)。
- 最坏情况下如果哈希冲突严重,复杂度可能退化到 O(n)。
选择时一般这样考虑:
- 如果需要按 key 有序输出,或者需要上下界查询、范围操作,选
map。 - 如果只关心快速查找,不关心顺序,通常选
unordered_map。 - 如果 key 类型复杂,使用
unordered_map时需要考虑哈希函数和冲突问题。
面试时最好补充一点:
unordered_map 虽然平均性能更高,但在工程里不一定绝对优于 map,因为哈希冲突、rehash、内存占用等问题也需要考虑。
8. 左值、右值、左值引用、右值引用分别是什么
参考回答:
这是现代 C++ 中比较重要的基础概念。
左值,一般可以理解为“有名字、可定位、生命周期较明确”的对象。
例如普通变量:
int a = 10;
这里的 a 就是左值,因为它有明确的内存位置,可以被取地址。
右值,一般可以理解为“临时值、即将销毁的值、不能长期持有地址的值”。
例如:
a + 1
5
std::string("abc")
这些通常都是右值。
左值引用使用 T& 表示,只能绑定左值:
int a = 10; int& ref = a;
右值引用使用 T&& 表示,主要用于绑定右值:
int&& rref = 10;
右值引用的重要意义在于支持移动语义和完美转发。
以前对象拷贝可能会带来额外开销,而引入右值引用后,可以“窃取”临时对象内部资源,减少不必要的深拷贝。
例如 std::vector 扩容或函数返回大对象时,移动语义能显著提升效率。
所以这个问题不能只停留在“谁能取地址”,更重要的是要理解它和 C++11 性能优化的关系。
9. 什么是移动语义,什么情况下会用到移动构造
参考回答:
移动语义是 C++11 引入的重要特性,目的是减少对象拷贝开销,特别是对于内部持有堆资源的对象。
传统拷贝构造会把源对象的数据完整复制一份,比如一个内部有动态数组的类,拷贝时需要重新申请内存并复制内容,这样
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。