金山 C++ 二面 面经

1. 自我介绍

二面里的自我介绍一般比一面更强调项目深度和岗位匹配,不需要讲太多基础课程,更建议重点说自己的项目经历、技术方向,以及为什么适合这个岗位。

参考回答:

面试官您好,我是 XX,目前主要方向是 C++ 后端开发。之前系统学习过 C++、操作系统、计算机网络、数据库等基础知识,也做过几个和服务端开发相关的项目,比如网络通信、高并发处理、线程池、缓存等方向的实践。在做项目过程中,我比较关注代码质量、性能优化和并发场景下的问题处理。这次也希望进一步和您交流一下我对 C++ 工程开发和系统设计方面的理解。

2. 讲一个你项目中最有难度的问题,以及你是怎么解决的

参考回答:

这个问题在二面中很常见,面试官更关注你解决问题的能力,而不只是“做了什么功能”。建议从“问题背景、问题表现、定位过程、解决方案、最终效果”几个角度来讲。

例如可以这样回答:

我项目中有一个比较典型的问题,是服务在并发量上来之后响应时间明显变长,偶尔还会出现请求堆积。最开始我先通过日志和压测数据确认问题主要集中在线程处理环节,进一步排查后发现是任务队列竞争比较严重,多个工作线程在高并发下频繁争抢锁,导致线程切换开销变大。

后面我做了两方面优化:一方面缩小锁的粒度,尽量减少临界区中的逻辑;另一方面把部分可以提前完成的处理放到加锁之外,同时对任务分发策略做了调整,降低热点竞争。

优化之后,吞吐量有明显提升,请求延迟也更稳定。

这个过程让我对并发场景下“锁竞争不是只有正确性问题,还有性能问题”有了更深的理解。

二面回答这种题,最好体现出你有排查路径,而不是只说“我优化了某个模块”。

3. 线程池的核心设计要点有哪些

参考回答:

线程池的核心目标是复用线程,避免频繁创建和销毁线程带来的开销,同时提高任务处理效率。一个比较完整的线程池通常需要考虑以下几个方面。

第一,任务队列。

线程池需要一个任务队列来缓存待执行任务,生产者负责投递任务,工作线程从队列中取任务执行。这个队列需要考虑线程安全。

第二,工作线程管理。

线程池初始化时会创建若干个工作线程,这些线程通常会持续运行,不断从任务队列中取任务执行,而不是执行一次就退出。

第三,同步机制。

为了保证多个线程同时访问任务队列时不出问题,通常需要使用互斥锁、条件变量等机制。

比如队列为空时,工作线程阻塞等待;有新任务时,通知某个或多个线程被唤醒处理。

第四,线程池关闭机制。

线程池不能只考虑启动,也要考虑优雅退出。比如停止接收新任务、等待已有任务处理完成、唤醒阻塞线程并安全回收线程资源。

第五,任务提交接口设计。

通常会设计一个通用提交接口,支持提交普通函数、lambda、可调用对象等,并且在需要时返回结果,比如结合 std::future 实现异步任务结果获取。

如果继续往工程角度深入,还可以考虑:

  1. 任务队列是否有界。
  2. 是否支持动态扩缩容。
  3. 是否区分 CPU 密集型和 IO 密集型任务。
  4. 是否支持任务优先级。
  5. 异常处理和任务超时机制。

4. 线程池里的任务队列为什么可能成为性能瓶颈

参考回答:

虽然线程池能提升整体效率,但如果设计不合理,任务队列本身也可能成为瓶颈,主要原因有以下几点。

第一,锁竞争。

如果所有工作线程和提交线程都访问同一个共享队列,那么在高并发场景下,大家会频繁竞争同一把锁。任务越多,竞争越激烈,线程切换和等待开销就越明显。

第二,单队列热点问题。

单个任务队列意味着所有任务提交和获取都集中在一个点上,这种集中式设计在低并发时简单有效,但在并发量高时容易形成热点。

第三,任务执行时间差异。

如果队列中混合了耗时很长和很短的任务,虽然线程池整体上在工作,但调度效果未必均衡,可能有些线程长期被长任务占用,而其他线程空闲,导致吞吐不稳定。

第四,生产速度和消费速度不匹配。

如果任务提交速度远大于工作线程处理速度,队列长度就会不断增长,导致内存占用增加,系统响应延迟上升。

常见优化思路包括:

  1. 缩小临界区,减少锁持有时间。
  2. 使用多队列设计,降低热点竞争。
  3. 引入无锁队列或更高效的并发队列。
  4. 对任务进行分类调度。
  5. 设置队列容量和限流策略,防止任务无限堆积。

这类题在二面中比较常见,因为它更偏“理解系统行为”,而不是只会写一个基础线程池。

5. 讲一下 std::mutexstd::unique_lockstd::lock_guard 的区别

参考回答:

这三个都和互斥锁使用有关,但定位不同。

std::mutex 是底层互斥量对象,本身负责加锁和解锁。

它提供 lock()unlock()try_lock() 等接口,但如果手动调用这些接口,需要自己保证异常安全和作用域退出时正确释放锁。

std::lock_guard 是一个非常轻量的 RAII 封装。

它在构造时加锁,在析构时自动解锁,适合简单、作用域固定的加锁场景。

它的优点是简单、开销小、不容易误用。

缺点是功能比较少,不支持手动解锁、延迟加锁、转移锁所有权等操作。

std::unique_lock 也是 RAII 风格的锁封装,但功能更灵活。

它支持延迟加锁、手动解锁、重新加锁、所有权转移等,还可以和条件变量配合使用。

例如 condition_variable::wait 就要求传入 unique_lock,因为等待过程中需要临时释放锁,再被唤醒后重新获取锁。

总结来说:

  1. 只想简单加锁保护一段代码时,用 lock_guard
  2. 需要更复杂的锁控制,比如配合条件变量、延迟加锁、解锁再上锁时,用 unique_lock
  3. mutex 是底层锁对象,一般和这两种封装类配合使用,而不是裸用。

6. 什么是虚函数表,多态是怎么实现的

参考回答:

C++ 中的运行时多态通常依赖虚函数机制实现。底层上,一般会通过虚函数表和虚函数表指针来完成动态绑定。

如果一个类中定义了虚函数,那么编译器通常会为这个类生成一张虚函数表,表中存放该类虚函数的入口地址。

同时,每个对象内部通常会有一个隐藏的虚函数表指针,指向所属类的虚函数表。

当我们通过基类指针或引用调用虚函数时,程序不会在编译期直接确定调用哪个函数,而是运行时根据对象实际类型,通过对象中的虚表指针找到对应虚函数地址,再完成调用。

例如:

Base* p = new Derived();
p->func();

这里虽然 p 的静态类型是 Base*,但它实际指向 Derived 对象,所以运行时会找到 Derived 对应虚表中的 func 实现。

这就是多态的本质:

接口统一,行为随对象实际类型变化。

需要注意几点:

  1. 多态成立需要通过基类指针或引用调用虚函数。
  2. 普通对象直接调用成员函数时,不体现运行时多态。
  3. 如果基类函数不是虚函数,就不会发生动态绑定,而是静态绑定。

7. overridefinal 有什么作用

参考回答:

这两个关键字都是 C++11 引入的,主要用于增强继承体系中的可读性和安全性。

override 用于显式说明“这个函数是对基类虚函数的重写”。

它最大的价值在于帮助编译器检查。

如果你本来想重写基类函数,但因为函数签名写错了,比如参数不同、const 不一致、返回类型不匹配,那么如果没有 override,编译器可能只会把它当作一个新的普通函数;而加上 override 后,编译器会直接报错。

所以 override 本质上是防止“以为自己重写了,实际上没有重写”的问题。

final 有两种常见用法:

  1. 修饰虚函数,表示这个函数在当前类中是最终版本,派生类不能再重写。
  2. 修饰类,表示这个类不能再被继承。

例如某些类已经不希望继续扩展,或者某个虚函数逻辑已经固定,就可以用 final 限制进一步修改。

在工程中,override 很推荐作为重写虚函数时的默认写法,因为它能降低继承代码出错的概率。

8. 什么情况下会发生对象切片

参考回答:

对象切片通常发生在“用基类对象去接收派生类对象”时,导致派生类特有的那部分数据被截断,只保留基类部分。

例如:

class Base {};
class Derived : public Base {};

Derived d;
Base b = d;

这里 b 是一个独立的 Base 对象,不再保留 Derived 的额外成员和行为,这种现象就叫对象切片。

对象切片的本质原因是:

基类对象本身

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

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

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

全部评论

相关推荐

04-10 18:32
已编辑
四川大学 Java
牛客17492028...:我只能说你这学历boss有的是人要,
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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