1. 自我介绍
  2. 用的编程语言是什么,别的编程语言有了解吗
  3. 讲讲在腾讯实习都做了什么
  4. 监控具体是怎么做的
  5. 讲一下常用的设计模式
  6. 单例模式有什么优点
  7. c++ map,set 的区别, unordered_map 呢
  8. 红黑树了解吗
  9. Multimap 了解吗
  10. 讲一下线程池,项目中有用过吗,怎么设计的,核心线程的概念是什么,核心线程用完了怎么办
  11. 讲一下内存池
  12. c++的 future 了解吗,async 呢
  13. 讲一下面向对象的三大特性
  14. 讲一下动态多态和静态多态
  15. 讲一下智能指针,在什么问题的背景下需要智能指针
  16. 讲一下 shared_ptr ,传参的时候值传递和引用传递对shared_ptr 引用计数的影响

参考答案(按原题编号)

原始 16 条面试题清单保持不变。以下按相同题号补充“口述参考 → 展开/追问 → 边界与待确认”,长题可按追问分段讲,不需要一次全部背完。技术题以 C++17 为主;“核心线程”部分明确区分 Java ThreadPoolExecutor 与 C++ 设计。

自我介绍及腾讯/科大讯飞经历依据你已有简历整理;示例代码用于解释机制,不代表实习源码。简历未明确的其他语言、资源采样 API/阈值、线程池及设计模式使用经历,明确标待确认,不编造项目或指标。

1.自我介绍

**口述参考:**面试官您好,我目前是安徽大学计算机技术专业的硕士研究生,预计 2027 年 6 月毕业,主要使用 C/C++,熟悉 C++17、STL、RAII 和多线程/异步编程。

在腾讯搜狗输入法 PC 客户端实习中,我参与了资源监控、UI 卡顿检测和流畅性优化,定位过跨进程互斥锁无限等待导致输入法及宿主卡死的问题,并通过有限等待的锁接口和 TLS 配置快照减少按键热路径的同步开销。

之前在科大讯飞智能汽车业务做过 C++ 开发,涉及车位管理、状态机和碰撞检测。这些经历让我比较关注实际场景下的资源管理、并发和稳定性,希望继续从事软件开发工作。

**展开/追问:**建议控制在 1 分钟左右。若追问项目,可以再介绍《杀戮尖塔 2》Game Agent;不用开场把所有经历和论文都讲完。

**边界/待确认:**教育、语言和实习内容依据已有简历;毕业时间说“预计”,不把简历上的未来日期说成已经毕业。

2.主要编程语言与其他语言

**口述参考:**我主要使用 C/C++,实习中以 C++ 客户端开发为主,熟悉 C++17、STL、RAII、多线程和异步编程。对我来说,C++ 不只是语法,还要掌握对象生命周期、所有权、并发同步和底层资源管理。

如果问具体用法,我可以结合客户端监控、卡顿定位、配置快照等经历解释为什么选择相应的容器和同步方式。

**展开/追问:**其他语言需你补一句真实回答:用过什么语言、在哪个项目用、能独立做到什么程度;“做过完整项目”和“了解基本语法”要区分。

**边界/待确认:**简历没有单列 Python/Java 的熟练程度,不因为使用了 LangGraph 或听过 Java 线程池就擅自写成“熟练 Python/Java”。

3.腾讯实习做了什么

**口述参考:**我在腾讯搜狗输入法 PC 客户端主要参与资源监控、UI 卡顿检测和流畅性优化。

资源监控覆盖 5 个核心进程,采集 CPU、内存、磁盘空间和磁盘 I/O 等指标,完成采集、聚合、上报,并将监控策略通过云控接入实验版本。基于这些数据定位了升级流程中的旧版 IME 文件残留,通过临时文件重试清理和版本保护逻辑调整,使单设备历史版本数 P99 从 4 降到 3。

UI 检测中设计了 Watchdog 线程,每 200ms 向 UI 线程发送异步探测消息,结合 ACK 时间戳和调用栈定位卡顿,发现热路径中的跨进程互斥锁缺少超时机制,可能使输入法及宿主应用卡死。

优化时在保持现有 ABI 兼容的前提下增加超时锁接口;通过焦点驱动的 TLS 配置快照,让 UI 热路径读取线程本地副本,结合异步落盘,按键路径加锁次数下降了 50%。

**展开/追问:**主线按“监控发现问题 → 定位根因 → 修改同步/清理逻辑 → 验证效果”讲;如问 AI Coding,可补充参与 SG Flow 的领域 Memory 和阶段 Hook 机制。

**边界/待确认:**50% 指按键路径加锁次数,不是延迟、CPU 占用或整体性能提升 50%;P99 的统计窗口、样本和基线需要按真实记录解释。

4.监控具体怎么实现

**口述参考:**我会把监控分成资源占用监控和 UI 卡顿检测,两条链路的目的、指标和周期不同。

资源监控覆盖 5 个核心进程,采集 CPU、内存、磁盘空间和磁盘 I/O,经过聚合、上报,用云控控制实验版本的监控策略;用采集数据去定位升级流程中的文件残留等问题。具体分析维度、采样周期和接口名称要以项目实际实现为准。

UI 检测使用独立 Watchdog 线程,每 200ms 用 PostMessage 发送异步探测消息;UI 线程处理后更新原子 ACK 时间戳,Watchdog 根据响应延迟做三级判定,卡顿时暂停 UI 线程并采集调用栈。

这种方式能把“主线程有没有及时处理消息”和调用栈结合起来,而不是仅凭 CPU 高低猜测卡顿。定位到跨进程锁无限等待后,再用超时接口和配置快照优化热路径。

**展开/追问:**Windows 实现参考(具体 API 需你确认):GetProcessTimes 返回累计用户态/内核态 CPU 时间,做两次采样差分,再除以同单位的墙钟时间;如果按整机归一化,还需考虑逻辑处理器数。先说明 CPU 百分比的统计口径。

内存可区分工作集 WorkingSetSize 与私有提交量 PrivateUsage,二者不是同一个指标。GetProcessIoCounters 返回进程 I/O 计数,差分可以算 I/O 速率,但不能直接等同于物理磁盘实际读写量;磁盘剩余空间又是另一类指标。

说明监控开销与可靠性:不要在 UI 热路径做重采样或阻塞上报;暂停采栈要控制时间、保证恢复,并避免采集逻辑再获取被暂停线程可能持有的锁。误报、上报限速和阈值如何确定也要准备。

**边界/待确认:**200ms 只在简历中明确用于 Watchdog 探活,不能自动当成资源采样周期;三个延迟阈值、具体采样 API、消息内容和上报协议目前仍待你确认。

5.常用设计模式

**口述参考:**我理解设计模式的重点是解决变化点和耦合问题,而不是记住名称后到处套用。常见的有工厂、策略、观察者、状态和单例。

工厂模式把对象的创建与使用解耦,例如根据配置创建不同处理器;策略模式把可替换的算法封装起来,让调用方不必塞满条件分支。

观察者模式是一对多的事件通知关系,例如 UI 或状态变化通知多个订阅者;要考虑订阅者生命周期、退订、回调线程和重入。

状态模式把不同状态下的行为交给状态对象;单例控制某类实例的唯一性,但要谨慎处理全局依赖、并发和销毁顺序。

**展开/追问:**答完定义后挑 1—2 个确实使用过的模式,按“原来有什么问题 → 如何拆分 → 有什么收益/代价”展开。

**边界/待确认:**你维护过 APA 状态机,但这不自动证明用了 GoF 状态模式;Watchdog 专用线程也不自动等于观察者模式。具体设计模式使用经历需按代码确认。

6.单例的优点与边界

**口述参考:**单例的主要优点是控制实例唯一,避免本来就需要统一管理的状态或资源出现多份不一致实例;同时提供统一访问入口,并可以做延迟初始化。

例如应用级服务注册表可以考虑单例,但这是设计示例,不代表我在实习里一定这样实现。只有业务确实要求唯一时才适合采用。

C++ 常见写法是函数内局部 static。C++11 起首次初始化具有线程安全保证,但对象内部的后续读写仍要自己同步。

**展开/追问:**缺点包括隐藏依赖、全局可变状态、单元测试难替换和静态对象销毁顺序问题;有时依赖注入比单例更合适。

**边界/待确认:**不要把“线程安全初始化”说成“整个单例所有方法都线程安全”。示例禁止复制,不讨论跨动态库的额外唯一性约束。

class Singleton {
public:
    static Singleton& instance() {
        static Singleton value; // C++11 起,首次初始化线程安全
        return value;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
};
 

7.map、set 与 unordered_map

**口述参考:**map 保存键值对,键唯一、按比较器有序;set 只保存键/元素,同样有序且不保留重复的等价键。它们适合有序遍历、范围查询,查找/插入/删除通常按 O(log n) 理解。

unordered_map 保存键值对,用哈希和等价比较组织唯一键,不保证有序;查找、插入、删除平均 O(1),最坏可能 O(n),适合不要求有序的按键检索。

选择时先看要不要存映射、要不要排序/范围查询,再考虑哈希质量、内存开销和访问模式。map/set 常见实现是红黑树,但标准不强制这一种树实现。

**展开/追问:**map/set 的键等价由比较器决定,即 !comp(a,b) 且 !comp(b,a);unordered_map 使用 KeyEqual,等价键必须有相同哈希值,但哈希相同不代表键等价。

map 的插入通常不使已有迭代器失效,erase 只影响被删除元素;unordered_map 的 rehash 会使迭代器失效,但不会仅因 rehash 就使已有元素的引用和指针失效。

operator[] 在缺键时会插入默认构造的映射值;只想查有没有时优先考虑 find,不要不小心改变容器。set 没有这种映射下标接口。

**边界/待确认:**复杂度是相应操作的保证/平均或最坏界,不把“哈希 O(1)”说成任何情况下都 O(1)。

8.红黑树

**口述参考:**红黑树是一种带颜色约束的平衡二叉搜索树,用局部旋转和重着色控制高度,让查找、插入、删除保持 O(log n)。

常见五条性质是:节点为红或黑;根为黑;NIL 叶子视为黑;红节点的孩子不能是红;从任一节点到其后代 NIL 叶子的路径上,黑节点数量相同。

这些约束让最长路径不会任意退化,n 个内部节点的高度不超过 2log2(n+1)。插入新红节点后主要修复红红冲突;删除黑节点后主要修复黑高不一致,具体通过重着色与旋转完成。

**展开/追问:**左旋/右旋改变局部父子关系但保持二叉搜索树的中序顺序。它不是完全平衡树,也不是每次修改都必须旋转。

**边界/待确认:**NIL 是概念上的黑叶子,可以由空指针或哨兵表示;不要断言所有实现都给每个 NIL 分配独立对象。

9.multimap

**口述参考:**multimap 是允许多个等价键的有序键值容器;map 只允许一个等价键。multimap 可以表示一个类别对应多条独立记录,并按键分组有序遍历。

获取某键对应的全部记录用 equal_range(key),返回半开区间,再遍历其中的元素。find 只给一个匹配位置,不能代表全部匹配记录。

erase(key) 删除所有等价键对应的元素;erase(iterator) 只删除指定元素。因为一个键对应多个值,multimap 没有 map 那样的 operator[] 或 at。

**展开/追问:**查找区间边界是 O(log n),遍历 k 条匹配记录再花 O(k);count 的成本也不能简单说成与匹配数量无关的 O(log n)。另一种选择是 map<Key, vector<Value>>,适合按键批量管理值,取舍要看访问和修改方式。

**边界/待确认:**有序指键的比较顺序,不代表映射值也自动排序。

10.线程池、核心线程与任务满载

**口述参考:**线程池的基本思路是复用一组工作线程,提交方把任务交给队列,worker 取出任务执行,减少每个任务都创建和销毁线程的开销,并限制并发度。

C++ 实现通常包括任务队列、工作线程集合、mutex、condition_variable 和停止状态;等待时用条件谓词处理虚假唤醒,持锁取任务后释放队列锁,再执行任务,不能拿着队列锁运行用户逻辑。

submit 可以配合 packaged_task/promise 返回 future;任务异常要反馈给提交方,不能让异常逃出线程入口导致进程终止。还要定义队列上限、满载策略,以及关闭时是排空还是取消待执行任务,notify/join 不应在阻碍 worker 退出的队列锁下进行。

“核心线程数”是池配置的常驻 worker 目标/阈值,不等于 CPU 核数。C++17/20 标准库并没有 Java ThreadPoolExecutor 那样统一的 corePoolSize/maximumPoolSize 机制,C++ 池要看具体库或自己的设计。

如果面试官指 Java ThreadPoolExecutor,典型正常接收任务的顺序是:少于 corePoolSize 时优先建线程;达到核心数后先尝试入队;队列满时才尝试扩到 maximumPoolSize;仍不能接收则走拒绝策略。不是“核心线程一忙就立即扩容到最大”。

无界队列通常使任务持续排队,maximumPoolSize 不会按想象发挥扩容作用;有界队列能配合最大线程数和回压控制资源。C++ 固定池满载时也可以等待、拒绝或由提交方执行,但不要让 UI 热路径因回压策略长时间阻塞。

**展开/追问:**线程数要按 CPU 密集/阻塞 I/O 等负载和实测决定,不能机械套一个公式;同时关注队列等待时延、满载率和任务吞吐。

避免所有 worker 都阻塞等待同一个池里尚未获得线程的子任务,否则可能发生线程饥饿死锁。

Java 默认按需创建核心线程,不代表构造线程池时就启动 corePoolSize 个;通常保留已创建的核心线程,但也允许配置核心线程超时回收。这不是 C++ 的语言规则。

**边界/待确认:**简历证实你做过专用 Watchdog 和多线程/异步,未明确是否实际用过或手写线程池。项目使用、库名、线程数、队列容量和拒绝策略请填真实情况;若未做过,要明确说了解原理,不把专用线程说成线程池项目。

11.内存池

**口述参考:**内存池预先或分批向底层取得较大内存,再按块管理和复用,适合大量频繁申请/释放、大小比较集中的对象。目标是减少底层分配次数和管理开销,改善某些负载下的局部性;不是任何场景都一定比系统分配器快。

简单的定长池可以用空闲块链表,分配时取一块、释放时归还;大小不同时可以用不同尺寸等级。还要处理对齐、耗尽时的扩容/失败策略以及并发访问。

内存分配和对象构造是两个层次:得到原始存储后仍要按规则构造对象,归还存储前要处理对象析构,不能把对象生命周期和字节分配混为一谈。

**展开/追问:**C++17 可以了解 std::pmr::unsynchronized_pool_resource 和 synchronized_pool_resource;前者不自带线程安全保证,后者保护的是内存资源的分配操作,不等于使用内存的对象就线程安全。内存资源应活得比使用它的对象/容器久,池分配与释放方式也必须匹配,不能随意交给普通 delete。

**边界/待确认:**如果追问项目中用过什么池,当前简历没有证据,先讲设计和取舍,不编造已经部署自研内存池或提升百分比。

12.future 与 async

**口述参考:**future 是取得异步结果的句柄,不等于线程本身;结果、就绪状态和异常放在共享状态中。promise 可以由生产者设置结果,packaged_task 能把可调用任务与结果关联起来,async 则负责按启动策略运行任务并返回 future。

future 通常不能复制但可以移动;wait 只等待,get 等待并取结果,任务抛出的异常会在 get 中重新抛出。get 后该 future 失效,不能当成可重复读取的结果容器;多个观察者可以各持一份 shared_future。

async 不指定策略时,默认允许 async 或 deferred,由实现选择,不能保证一定立即另起线程。显式 launch::async 按异步执行语义运行;launch::deferred 在第一次非定时等待时,由进行该等待的线程执行,wait_for/wait_until 本身不会触发 deferred 任务。

某些由 async 异步执行产生的共享状态,在最后一个持有者释放时可能需要等待任务结束;所以直接丢弃临时 async 返回值可能使调用看起来串行。不是所有来源的 future 析构都等待。

**展开/追问:**std::async 不保证使用线程池,也没有线程池的队列容量/拒绝策略接口;大量任务需要明确并发控制。捕获引用时保证被引用对象活得足够久,不能把任务对象生命周期交给侥幸。

**边界/待确认:**shared_future 可共享结果状态,不表示可以无条件并发修改同一个句柄或结果对象;无效 future 上重复 get 不属于合法用法。

#include <future>
 
int example_async() {
    auto result = std::async(std::launch::async, [] {
        return 40 + 2;
    });
    return result.get(); // 等待并取得结果;get 后该 future 失效
}
 

13.面向对象三大特性

**口述参考:**三大特性是封装、继承和多态。封装把状态与行为放在类型内部,通过受控接口维持不变量,而不是让外部随意改数据。

继承表达类型之间的 is-a 关系,派生类在满足接口契约的前提下扩展或复用基类能力;仅仅为了复用代码不一定该用继承,有时组合更合适。

多态让统一接口对应不同实现。运行时多态依靠虚函数重写和对象的动态类型;重载、模板等可以形成编译期选择的静态多态。

**展开/追问:**不要把继承和多态混为一谈:有继承关系不代表所有成员函数都是动态绑定;private/protected/public 只是封装手段之一。

**边界/待确认:**举项目例子时要说明实际类关系,不擅自把所有函数接口都说成继承体系。

14.动态多态与静态多态

**口述参考:**动态多态的典型情况是用基类指针/引用访问派生对象,调用被重写的虚函数,按对象的动态类型决定最终执行哪个实现,适合运行时替换实现或管理异构对象。

静态多态通常通过重载、模板或 CRTP 在编译期根据类型信息选择/生成实现,往往便于内联,但模板实例化也可能带来代码体积和编译时间成本。

主流编译器常用虚表/虚表指针实现虚调用,但 C++ 标准不强制这一布局,也不是每个类都带虚表。动态多态有潜在分派开销,编译器也可能去虚化,不能简单说每次一定查表或一定更慢。

**展开/追问:**重载决议发生在编译期,选中的函数若为虚函数,还可能进行动态分派;override 是编译期检查,派生类不再次写 virtual 也仍可重写。经基类指针普通 delete 派生对象时应满足虚析构等条件。

**边界/待确认:**模板函数里仍可能有运行时逻辑,静态多态不等于整个函数都在编译期计算;构造/析构期间和限定名调用也有虚分派边界。

15.为什么需要智能指针

**口述参考:**当代码有多个返回路径、异常或跨模块的所有权关系时,裸 owning 指针容易出现忘记释放、重复释放和生命期不清晰。智能指针通过 RAII,让资源释放跟拥有者生命周期关联起来,主要解决所有权和生命周期管理。

unique_ptr 表达独占所有权,不能复制但能移动,默认优先用于唯一拥有者;shared_ptr 表达共享所有权,最后一个强拥有者释放时销毁所管理对象,代价包括控制块和计数维护。

weak_ptr 是共享所有权体系里的非拥有观察者,不增加强引用计数,用 lock 尝试获得临时强拥有者;适合回指和观察关系,可以按合理所有权方向打破 shared_ptr 循环引用。

**展开/追问:**优先考虑 make_unique/make_shared;避免同一个裸指针分别构造两个独立 shared_ptr 控制块。weak_ptr 的 expired 检查后对象可能变化,真正使用时以 lock 的结果为准。

**边界/待确认:**智能指针不能自动解决越界和任意数据竞争;shared_ptr 控制块的计数安全不等于对象字段或同一 shared_ptr 变量的任意并发修改安全。

16.shared_ptr 参数传递与引用计数

**口述参考:**以同类型、非空 shared_ptr 左值 sp 为例,假设调用前强引用计数为 N:按值参数会复制一份拥有者,参数构造后通常为 N+1;如果函数不保留额外副本,参数释放后回到 N。

const shared_ptr<T>& 或 shared_ptr<T>& 的引用绑定本身不增加强引用计数。但函数内部若复制、保存或 reset 指针,计数和指向还会按相应操作变化;非 const 引用可以改变调用方的句柄。

如果按值传入 std::move(sp),移动本身通常不增加计数,而是把拥有关系转移给参数,调用方的 sp 变为空。不能把“按值传 shared_ptr”绝对化为每次都加一。

函数只是使用对象、不保留所有权时,可以考虑 T&/const T& 或允许空值的 T*;需要在返回后或异步任务里继续持有对象时,应明确取得/保存 shared_ptr 的强所有权。

**展开/追问:**const shared_ptr<T>& 是句柄引用的 const,不等于 T 是 const;需要只读对象类型时可考虑 shared_ptr<const T>。

上述计数变化假设同类型传递且没有额外拥有者变化;隐式转换创建临时 shared_ptr 或函数内部复制时需另算。use_count 的瞬时结果不能拿来证明并发访问安全。

**边界/待确认:**参考代码固定在单线程、初始只有一个拥有者的条件下观察计数;不要把 1/2 当成任何真实并发场景的固定值。

#include <cassert>
#include <memory>
#include <utility>
 
long count_by_value(std::shared_ptr<int> p) {
    return p.use_count();
}
long count_by_reference(const std::shared_ptr<int>& p) {
    return p.use_count();
}
 
void example_counts() {
    auto p = std::make_shared<int>(42); // 仅有这一个拥有者
    assert(p.use_count() == 1);
 
    const auto copied_count = count_by_value(p);
    assert(copied_count == 2);
    assert(p.use_count() == 1); // 参数已释放,没有保存额外副本
 
    const auto referenced_count = count_by_reference(p);
    assert(referenced_count == 1);
 
    const auto moved_count = count_by_value(std::move(p));
    assert(moved_count == 1);
    assert(!p); // 不是加一,而是把拥有关系转交给参数
}
 

技术参考与需要你补的细节

**个人细节:**其他语言的实际熟练程度;资源监控采样周期和具体 API;卡顿三级阈值;P99 和加锁次数的统计窗口、基线;是否实际使用线程池/内存池、具体库和参数;哪些设计模式确实用于代码。未确认的内容不要照着模板说成已完成的项目。

技术校对参考:std::async;std::future;Java ThreadPoolExecutor;Windows CPU 时间;Windows 内存指标;Windows 进程 I/O。