私募面试准备记录
上海某私募 cpp 开发面试准备记录,deepseek提问的问题
准备的问题
1. 用一句话解释什么是RAII?它在C++中解决了什么根本问题?
追问方向:
RAII全称Resource Acquisition Is Initialization,核心是利用对象生命周期管理资源(构造函数获取,析构函数释放),解决异常安全和资源泄漏问题。面试官会追问:“如果没有RAII,C++中如何保证异常安全?”(答:用try-catch手工释放,极易遗漏)
答:
RAII(资源获取即初始化)的核心是将资源的生命周期与对象的生命周期严格绑定,在构造函数中获取资源,在析构函数中自动释放资源。
它从根本上解决了资源泄漏和异常安全问题——确保即使在抛出异常或提前返回等复杂控制流下,堆内存、文件句柄、锁等资源也能被自动、确定性地回收。比如在我做的 Bustub 项目中,正是利用 RAII 管理 BufferPool 的 Page 锁和内存页,保证任何异常路径下资源都能正确归还,避免死锁或泄漏。
如果没有RAII,为了保证异常安全,就只能手动使用 try-catch 块,在捕获异常和正常退出路径中都显式调用释放函数(如 delete、fclose、pthread_mutex_unlock)。
2. 你在Bustub项目中具体哪里用到了RAII?能讲一段代码设计吗?
追问方向:
比如PageGuard或TransactionGuard——析构函数里自动Unpin Page或Rollback事务。面试官会追问:“如果一个PageGuard对象被拷贝了,会发生什么?你怎么处理的?”(引出拷贝构造函数和赋值操作符的delete,或正确实现移动语义)
3. std::lock_guard和std::unique_lock的区别是什么?它们各自的应用场景?
答:
lock_guard:轻量级,仅RAII上锁/解锁,不可移动,不可手动解锁。
unique_lock:支持延迟锁定、手动解锁、移动、条件变量(wait需要它)。
在低延迟场景下优先用lock_guard(开销更小),但需要条件变量、中途解锁、尝试锁定或转移锁所有权时不得不用unique_lock。
4. shared_ptr的引用计数是如何实现的?它是线程安全的吗?
追问方向(三层深度):
第一层:控制块(Control Block)存储强引用计数和弱引用计数,通过原子操作(std::atomic)增减。
第二层:引用计数本身的增减是线程安全的,但指向的对象不是——多个线程同时修改对象内容仍需加锁。
第三层:make_shared和new shared_ptr的区别?make_shared一次内存分配(对象+控制块一起),性能更好且更安全(避免内存碎片),但控制块和对象绑定,若存在weak_ptr,即使对象销毁,控制块内存也无法立即释放(直到最后一个weak_ptr销毁)。
答:
第一层:引用计数如何实现
std::shared_ptr 的引用计数并不存在对象内部,而是存储在堆上的独立控制块(Control Block)中。控制块里至少维护两个原子计数器:
强引用计数(use_count):持有 shared_ptr 实例的个数,为0时销毁托管对象。
弱引用计数(weak_count):持有 weak_ptr 的个数,用于控制块自身的生命周期。
每次拷贝构造或拷贝赋值时,通过 原子操作(Atomic Operation) 对计数器进行 fetch_add(1);析构时 fetch_sub(1) 并检查是否为0,若为0则执行删除器并(或)释放控制块。
第二层:线程安全问题(关键区别)
必须严格区分两个层面:
引用计数本身的增减是线程安全的。多个线程同时拷贝同一个 shared_ptr 或销毁各自的副本,不会产生数据竞争,计数器的最终值一定正确,资源也只会被释放一次。这是标准库保证的。
但 shared_ptr 管理的对象数据绝对不是线程安全的。如果两个线程同时通过 ptr->method() 或 *ptr 修改托管对象,这会导致未定义行为,必须由用户自己加外部锁(如 std::mutex)或确保对象内部是线程安全的(如使用原子变量)。
还有一个容易被忽视的陷阱:同一个原始指针被两个独立的 shared_ptr 管理(即 shared_ptr
5. unique_ptr能拷贝吗?怎么转移所有权?它在低延迟场景下比shared_ptr好在哪里?
答:
不能拷贝,只能移动(std::move)。低延迟场景下unique_ptr零开销(没有引用计数原子操作),而shared_ptr的原子增减在极端并发下可能成为瓶颈(Cache Line频繁争用,引发性能抖动)。
6. weak_ptr是干什么的?怎么解决循环引用?能举例吗?
答:
weak_ptr不增加引用计数,用于打破shared_ptr循环引用(如双向链表节点互相持有)。使用时需要调用lock()临时提升为shared_ptr来访问对象。追问:“如果lock()返回空指针代表什么?”(对象已被销毁)
7. 用shared_ptr实现线程安全的引用计数,底层用的是std::atomic的哪种内存顺序?是memory_order_seq_cst还是memory_order_acq_rel?
答:
增加引用计数(拷贝构造/赋值):使用 memory_order_relaxed。
减少引用计数(析构):使用 memory_order_acq_rel(或 release + acquire 的组合,视具体平台优化而定)。
标准库通常使用memory_order_acq_rel(获取-释放语义)而非顺序一致性,性能更好。
递减引用计数时用memory_order_acq_rel,判断是否为0时用memory_order_acquire。
8. C++的多态有几种?静态多态和动态多态分别是什么?
答:
静态多态:函数重载、模板(编译期决议,零运行时开销)。
动态多态:虚函数(运行时决议,通过虚函数表vtable)。
在低延迟交易系统中,尽量避免虚函数(原因见下一题)。
9. 虚函数的调用开销具体包含哪些?为什么低延迟系统要避免虚函数?
答:
开销1:间接寻址(通过vptr→vtable→函数指针,多一次内存跳转)。
开销2:无法内联(编译器不知道具体调用哪个函数,无法做内联优化)。
开销3:分支预测失败(如果vtable指向的函数地址动态变化,CPU分支预测器很难命中)。
在交易系统热路径(如每秒处理百万级订单)中,虚函数调用可能导致纳秒级抖动累积成微秒级延迟。
10. final关键字和override关键字的作用是什么?
答:
override显式标明重写父类虚函数,让编译器帮你检查签名是否正确;final阻止进一步重写,允许编译器做去虚拟化(Devirtualization)优化——如果编译器确定对象的具体类型,可以直接调用具体函数而跳过vtable,实现零开销。这在交易系统中极其重要。
11. 构造函数和析构函数中可以调用虚函数吗?为什么?
答:
可以调用,但不会产生多态效果——调用的是当前类自己的版本,不是派生类的重写版本。因为在构造/析构期间,派生类的vptr尚未初始化或已被销毁。
12. vector的扩容机制是怎样的?reserve和resize的区别?
答:
扩容通常按2倍或1.5倍增长(不同编译器实现不同),涉及重新分配内存 + 移动/拷贝元素。
reserve只分配容量(capacity),不构造元素;resize既分配容量又构造元素(默认值)。
在低延迟场景中,预分配(reserve)至关重要,避免热路径中触发扩容导致的延迟尖刺。
13. vector中存放bool类型有什么特殊之处?有什么问题?
答:
vector
14. deque的底层实现是什么?push_front和push_back的复杂度是多少?
答:
deque是分段连续空间(由中央控制块Map管理多个固定大小的Buffer),push_front/push_back均为O(1)。追问:“deque的operator[]比vector慢在哪里?”(多一次地址计算和Map间接寻址)
15. std::map和std::unordered_map的选择依据是什么?
答:
map(红黑树):有序、稳定O(log n)、内存开销小(节点存储颜色和父子指针),适合范围查询。
unordered_map(哈希表):平均O(1)、最坏O(n)、内存开销大( buckets + 节点),适合单点查询。
低延迟场景下:unordered_map通常更快,但要警惕最坏情况(大量哈希冲突引发延迟尖刺),需要用自定义哈希函数(如使用std::hash + 随机种子)防止Hash DoS攻击。
追问:“你知道absl::flat_hash_map和std::unordered_map的区别吗?”(答:Google的Abseil使用Swiss Table,探测链更短,缓存友好性更好,低延迟场景常用)
16. 使用std::string在低延迟场景下有什么隐患?
答:
小字符串优化(SSO,Small String Optimization):短字符串(通常15字节以内)存储在栈上,长字符串会堆分配,导致不可预测的内存分配延迟。
在交易系统中,避免std::string热路径使用,改用定长字符数组或std::string_view(零拷贝只读)。
std::string_view在C17引入,目标岗位虽然是C11,但你可以说“如果项目升级到C++17,我会推荐使用string_view来减少拷贝”。
17. 移动语义和右值引用解决了什么问题?std::move的本质是什么?
答:
解决临时对象拷贝开销问题(如函数返回大对象)。
std::move本质是强制类型转换(将左值转为右值引用),并不移动任何东西,真正的移动发生在移动构造函数或移动赋值操作符里。
追问:“移动构造函数如果不加noexcept,vector扩容时会发生什么?”(经典考题:vector为了保证强异常安全,如果移动构造没有noexcept,扩容时宁可拷贝也不移动,导致性能回退)
18. 完美转发(Perfect Forwarding)是什么?std::forward和std::move的区别?
答:
std::forward在模板中保持参数的值类别(左值/右值)不变,实现完美转发。区别:std::move无条件转右值,std::forward有条件转右值(仅当参数原本是右值时)。
19. Lambda表达式在底层是怎么实现的?捕捉列表[=]和[&]分别有什么风险?
答:
底层编译为仿函数(Function Object)类,operator()默认是const(除非用mutable修饰)。
[=]值捕获存在拷贝开销和悬垂引用风险(捕获的是当前值,非引用)。
[&]引用捕获存在悬垂引用风险(如果被捕获的局部变量在lambda执行前销毁)。
在低延迟系统中,尽量用[&]并确保生命周期,或显式列出捕获变量以避免捕获不必要的内容(减少仿函数对象大小)。
20. std::function和裸函数指针的区别?它的开销在哪里?
答:
- 内存分配(最大的“毛刺”源头)——堆分配
std::function 内部通常实现 SBO(小对象优化,Small Buffer Optimization)。例如在 libstdc++ 中,这个缓冲大小通常为 16 字节(或 32 字节)。
如果捕获的 Lambda 或仿函数对象大小 超过这个缓冲,构造函数就会在堆上 new 一块内存 来存储这个可调用对象。
在交易系统的热路径中,一次意外的 new 操作(触发系统调用和锁)会直接产生微秒级的延迟毛刺,这是绝对不可接受的。 - 调用时的间接性与内联失效(指令缓存污染)
调用 std::function 不是直接跳转到目标地址,而是:
通过 operator() → 内部的虚函数表(vtable)或函数指针表 → 间接跳转到真正目标代码。
无法内联(No Inlining):编译器无法在编译期知晓 std::function 内部装的是什么类型,因此无法进行内联展开。对于像 Compare 或 ForEach 这类高频调用的短逻辑,失去内联意味着函数调用本身(栈帧压栈/弹栈)就会消耗几十个时钟周期。
分支预测干扰:间接跳转(Jump via register)会占用分支目标缓冲器(BTB),相比直接函数调用或内联,更容易导致 CPU 流水线清空(Pipeline Flush)。 - 拷贝时的额外开销
std::function 是值语义的。当你拷贝它时,如果内部对象在堆上,会执行深层拷贝(复制堆上的仿函数对象)。如果回调对象很大(如包含大量捕获的上下文),拷贝开销会非常显著。
21. C++对象的内存布局是怎样的?虚函数表(vtable)存在哪里?
答:
对象开头是vptr(如果有虚函数),紧接着是成员变量(按声明顺序,存在对齐填充)。vtable是每个类一份的静态数据,存在只读数据段(.rodata)。追问:“多重继承下有几个vptr?”(多个基类有几个就有几个)
22. 什么是内存对齐(Alignment)?为什么需要它?怎么手动指定对齐?
答:
CPU访问未对齐内存可能触发总线错误或性能下降(需要多次读取拼凑)。
用alignas关键字指定对齐,或通过#pragma pack紧凑排列(但可能降低访问速度)。
在交易系统中,将高频访问的成员变量放在结构体前面,并合理排列字段顺序(从大到小)来减少对齐填充,提高缓存利用率。
23. 什么是假共享(False Sharing)?在交易系统中如何避免?
答:
多个CPU核心修改同一Cache Line中不同变量,导致Cache Line反复失效(RFO,Read For Ownership),引发缓存一致性协议的锁竞争,性能暴跌。
解决方案:用填充(Padding)将高频写入的变量隔离到不同的Cache Line(通常64字节)。
C11可以用alignas(64)或std::hardware_destructive_interference_size(C17)来规避。
24. 动态内存分配(new/delete)为什么在交易系统的热路径中是禁忌?怎么替代?
答:
new/delete调用底层malloc/free,涉及堆锁(Heap Lock)和系统调用(brk/mmap),延迟可达数百纳秒到微秒,且不可预测。
替代方案:内存池(Memory Pool) / 对象池(Object Pool),预先分配大块内存,用自由链表(Free List)管理,分配/释放O(1)且无锁(可用std::atomic做无锁栈)。
你可以结合Bustub的BufferPool说:“我在Bustub中管理Page就是用类似内存池的思想,所有Page预分配,通过LRU-K复用,避免热路径中的动态分配。”
25. 你常用的C++调试工具和工作流是什么?
答:
GDB命令:bt(调用栈)、p(打印变量)、watch(设置观察点)、thread apply all bt(所有线程调用栈)。
内存检测:AddressSanitizer(ASan,编译加-fsanitize=address)检测越界和Use-After-Free;Valgrind(Memcheck)但速度慢。
性能分析:perf(Linux原生)、Flame Graph(火焰图)定位热点函数。
26. 怎么调试一个多线程死锁问题?
答:
gdb attach到进程,info threads查看所有线程。
thread apply all bt查看每个线程的调用栈,找到等待锁的线程。
查看锁的持有者(如std::mutex无法直接看,可以用gdb的print查看地址,或用helgrind/tsan(ThreadSanitizer)静态检测)。
实践中最有效的是:在代码里加日志打印“尝试获取锁X”和“成功获取锁X”,通过时间戳还原锁获取顺序。
27. 你在华为把单元测试覆盖率从60%提到75%,在C++项目中你会用什么框架和策略?
答:
框架:Google Test (GTest) + Google Mock (GMock)(C++最主流)。
策略:隔离外部依赖(文件系统用std::istringstream mock,网络用接口类+Mock)、测试边界条件(空指针、极大值、极小值)、并发测试(重复运行多次 + 设置超时断言)。
覆盖率工具:gcov + lcov(生成HTML报告)。
28. 设计一个线程安全的无锁栈(Lock-Free Stack),用C++11实现push和pop。
答:
用std::atomic<Node*>头节点,push中用compare_exchange_weak(或strong)循环CAS。
注意ABA问题:解决方法是使用带版本号的指针(std::atomic<std::pair<Node*, int>>),或使用std::atomic<std::shared_ptr>(但有开销)。
追问:“compare_exchange_weak和compare_exchange_strong的区别?为什么无锁队列中常用weak?”(weak允许伪失败(Spurious Failure),在循环中效率更高)
29. 你提到“理解多态”,那你了解CRTP(奇异递归模板模式)吗?它和虚函数多态有什么不同?
答:
CRTP用于实现静态多态(编译期绑定),没有vtable和虚函数开销,适合低延迟场景。
示例:template
结合你的Bustub项目:如果想让执行器算子(Executor)不用虚函数Next(),可以用CRTP + 模板来替代,实现零开销抽象。
30. 最后一个硬核问题:C++的volatile关键字能用于并发编程吗?为什么?
答:
不能!volatile只告诉编译器“不要优化这个变量,每次都从内存读”,但不提供原子性和内存顺序保证。
并发编程必须用std::atomic,它有内存屏障(Memory Barrier)保证可见性和顺序性。
面试官可能会追问:“Java的volatile和C的volatile有区别吗?”(Java的volatile有Happens-Before语义,而C的没有,完全不可比)
高频手撕代码题汇总(建议白板/纸上练习)
题目 对应C知识点 难度
实现shared_ptr的简化版(含引用计数) 智能指针、原子操作
实现unique_ptr(含移动构造/赋值) 移动语义、Rule of Five
实现一个简易的内存池(Memory Pool) 内存管理、链表
手写std::vector的简化版(含扩容) 模板、RAII、拷贝/移动
用CAS实现无锁栈(Lock-Free Stack) 原子操作、内存顺序
实现一个线程安全的单例(C11 call_once版本) 多线程、C++11特性
1. 独立负责与架构设计
“‘独立负责认证鉴权微服务的需求迭代与代码仓维护’——这个服务有多大?日均QPS多少?有多少个POD实例?”
追问方向:
如果QPS突然翻10倍,你的服务会先挂在哪里?(连接池不够?CPU打满?DB连接数爆了?)你做过压测吗?阈值是多少?
2. “基于SpringBoot + OAuth2.0/CAS3.0,你用的是Spring Security框架还是自研封装?为什么选这个方案?”
追问方向:
OAuth2.0的四种授权模式你们用了哪几种?客户端凭证模式(Client Credentials)和授权码模式(Authorization Code)在微服务间调用的场景下怎么选?
3. “支持RBAC和ABAC的细粒度访问控制——ABAC的规则引擎是怎么设计的?规则配置是存在数据库还是本地配置文件?”
追问方向(关键性能问题):
每次鉴权请求进来,如果都要查数据库拉取ABAC规则再做策略评估,延迟有多高?如果做了缓存,缓存失效时怎么保证不把DB打爆?(缓存雪崩/击穿如何防护?——这是微服务高并发通用问题)
4. 重构老旧逻辑(展示技术判断力)
“‘重构部分老旧鉴权逻辑’——你为什么觉得它老?重构时采用的策略是‘绞杀者模式’(逐步替换)还是‘大爆炸式’(全量重写)?”
追问方向:
重构过程中如何保证新旧逻辑在灰度期间行为一致?有没有做流量回放对比?如果重构后某个接口的行为和旧版本有细微差异,你怎么发现和决策的?
5. “三方登录模块重构支持多协议接入——之前支持什么协议?新增了哪些协议?”
追问方向:
OAuth2.0、CAS、SAML、OIDC这些协议的区别是什么?接入一个新协议时,抽象层(Adapter Pattern)怎么设计才能不修改核心鉴权逻辑?你用了什么设计模式?
6. 服务底座与联调(体现大局观)
“你的服务作为X项目的鉴权底座——‘底座’意味着什么?其他微服务是怎么调用你的鉴权接口的?是同步RPC还是异步事件?”
追问方向:
如果你的鉴权服务挂了,X项目会全部不可用吗?你做了哪些降级/熔断/限流措施?(比如Sentinel/Hystrix)有没有遇到过因为你的服务响应慢导致上游线程池耗尽的情况?
“和其他微服务完成联调,保障X项目稳定上线无重大事故——联调中最难协调的问题是什么?”
追问方向:
多个服务之间接口定义不一致、环境隔离、数据mock……举个例子你是怎么解决的?
7. OnCall与稳定性(体现责任心)
“参与现网OnCall,沉淀多篇常见问题文档——你遇到过最严重的现网故障是什么?根因是什么?你怎么排查和修复的?”
追问方向(STAR法则必考题):
比如OOM、死锁、慢SQL、缓存穿透?故障响应时间(MTTR) 多久?事后写了事故报告吗?怎么避免同类问题再发生?
“文档里最常见的问题TOP3是什么?有没有从文档中抽象出自动化修复工具?”
追问方向:
比如“Token过期怎么刷新”、“权限不足怎么排查”——你有没有把这些常见问题做成自动化巡检脚本或者自助诊断页面?
8. 单元测试覆盖率 60% → 75%(和岗位要求高度匹配)
“目标岗位明确要求‘熟练应用单元测试方法’,你在华为具体是怎么把覆盖率从60%提升到75%的?”
追问方向(深度拷打):
是用Jacoco统计的吗?哪些包/类覆盖率最低,你重点补了哪些?
对于依赖外部服务(如数据库、Redis、第三方OAuth服务)的类,你是怎么Mock的?(Mockito? @MockBean?)
提升这15%的过程中,有没有发现过真实Bug?(如果答“没有”,面试官可能觉得只是凑指标)
新增的测试用例主要是单元测试还是集成测试?你怎么区分二者的?
这75%的覆盖率能代表质量吗?你觉得覆盖率数字有什么陷阱?(比如只测了Happy Path,异常分支没测到)
“交易系统对代码正确性要求极高,你认为单元测试在低延迟系统中需要额外注意什么?”
追问方向:
多线程并发下的测试怎么保证可复现?时间敏感的测试(比如超时逻辑)怎么避免Flaky(不稳定)?
9. 动机类
“你之前在游戏客户端(C++)和云服务(Java)都有经验,现在来面低延迟C++交易系统——你职业生涯的技术主线是什么?为什么不继续做Java微服务架构师?”
期望回答方向:不是“Java没前途”,而是“我对性能极致优化一直有 passion,ACM竞赛种下了种子,Bustub项目强化了数据库内核兴趣,华为的经历让我具备了工业级工程能力,现在想回归C++系统层做真正的高性能计算”。
“低延迟交易系统和互联网微服务的核心区别是什么?你觉得你的Java经验哪些可以复用,哪些是负资产需要清零?”
期望回答方向:
可复用:并发编程模型(线程池、锁、CAS)、分布式一致性、稳定性治理(限流熔断降级)、单元测试/CI/CD、OnCall方法论。
需要清零/调整:JVM的GC暂停(必须手动内存管理)、微服务的网络开销(交易系统要避免RPC,尽量共享内存)、SpringBoot的“重”框架(交易系统用轻量级甚至裸写)。
10. 性能认知类(最高频)
“你在华为做的鉴权服务,一个请求的P99延迟大概多少毫秒?如果突然要求优化到微秒级,你会从哪些方面着手?”
追问方向:
GC优化(消除Young GC/Full GC)、锁优化(从ReentrantReadWriteLock改为StampedLock或无锁)、序列化优化(从JSON改为Protobuf/FlatBuffers)、IO优化(从同步阻塞改为Netty异步非阻塞)、缓存优化(从Redis远程缓存改为本地Caffeine缓存)。——主动展示你对“极致性能”的技术储备。
“C和Java在内存模型上最大的不同是什么?如果让你用C实现一个类似ConcurrentHashMap的并发容器,你会怎么设计?”
追问方向:
C++没有内置的读写锁分段(如Java的Segment锁),你要自己实现分段锁 + std::atomic + 内存顺序(memory_order)。你答得越底层,面试官越认可。
“交易系统的网络通信通常用UDP组播而不是TCP,你知道为什么吗?”
追问方向:
TCP的流量控制、拥塞控制、重传会导致延迟不可控(尾部延迟高),而UDP组播可以实现数据并行分发且延迟稳定。如果不知道这个问题,坦诚说“了解有限,但我知道TCP的Nagle算法和延迟ACK都会增加延迟”,说明你有基础认知。
11. C能力自证(既然简历写“熟悉C”)
“你最近一年多在写Java,C会不会生疏了?你怎么保持C能力的?”
追问方向:
除了Bustub,有没有自己写一些小项目/LeetCode用C++?有没有关注C17/20的新特性(如std::jthread、协程co_await、概念concepts)?你对C的内存顺序模型(Relaxed/Acquire-Release/SeqCst)了解多少?
“Java的ConcurrentHashMap和C++的std::unordered_map + 互斥锁,在高并发场景下性能差距有多大?为什么?”
追问方向:
JVM对synchronized做了偏向锁→轻量级锁→重量级锁的升级优化,而C的std::mutex直接走futex系统调用(开销大),所以C里更多用无锁编程或读写锁分段。
高频手撕代码题汇总(建议白板/纸上练习)
题目 考察点 关联经历
实现一个线程安全的本地缓存(带过期时间) 并发 + 定时清理 华为鉴权Token缓存
实现一个简单的速率限制器(令牌桶/漏桶) 高并发限流 华为服务保护
手写单例模式(懒汉+双重检查锁+C11call_once) C多线程安全 通用
设计一个配置中心客户端(拉取+本地缓存+热更新) 微服务配置管理 华为ABAC规则加载
用GDB调试多线程死锁,你会用什么命令? 调试能力 雷火/华为OnCall\
1. 你为什么选择做Bustub这个项目?从头实现一个数据库内核最难的地方是什么?
追问方向:
是课程作业还是自学?遇到最大的设计反复是什么?(比如先写BufferPool还是先写Catalog?)
2. 如果让你重新设计Bustub,你会做哪些架构层面的改动?
追问方向:
针对低延迟场景,你觉得Bustub的设计有什么不适合的地方?(这是一个绝佳的展示点——可以联系交易系统说“火山模型拉取数据开销大,或许更适合用push模型或JIT”)
3. Bustub整体模块划分是怎样的?各个模块(BufferPool、B+Tree、Executor、LockManager)之间如何交互?
追问方向:
画个简图说说一条SELECT * FROM users WHERE id = 1的SQL从输入到输出的完整调用链路。
4. LRU-K和普通LRU的核心区别是什么?为什么数据库用LRU-K而不是LRU?
追问方向:
K值怎么选?你实现的时候K=2,为什么?LRU-K的时间戳维护开销大吗,你怎么优化的?
5. 你的BufferPool中Page的生命周期是怎样的?什么时候从磁盘读进来,什么时候刷回去,什么时候被驱逐?
追问方向:
脏页(Dirty Page)怎么处理?被驱逐时如果正在被其他线程访问怎么办?(引出Pin/Unpin机制)
6. 你提到“所有Page操作在BufferPool上实现”,那B+树的查找是先查磁盘还是先查BufferPool?
追问方向:
如果Page在BufferPool中被淘汰了,B+树节点指针还在,怎么处理这种“悬空指针”问题?
7. BufferPool的并发安全怎么保证?多个线程同时请求同一个Page时怎么处理?
追问方向:
如果多个线程同时修改同一个Page的不同元组,你用的是Page级别的锁还是元组级别的锁?为什么?
8. LRU-K的实现中,你怎么避免“缓存污染”(比如大规模全表扫描把热数据挤出去)?
9. B+树的插入和分裂逻辑能讲一下吗?特别是分裂时的加锁策略。
追问方向:
你是乐观锁(先不加锁往下搜)还是悲观锁(全程加锁)?如果并发插入时发生页分裂,怎么保证其他读线程不读到不一致的状态?
10. B+树的删除操作中,什么时候需要合并兄弟节点?合并后父节点怎么更新?
追问方向:
如果合并时父节点也underflow了,需要递归合并吗?你实现了级联合并还是懒惰合并?
11. 你的B+树支持并发读写吗?用的是 latch(闩锁)还是 lock(锁)?
追问方向(高频考点):
latch和lock有什么区别?(latch保护临界区,lock保护事务逻辑;latch死锁要避免,lock死锁可检测回滚)
12. 有没有考虑过B+树的缓存友好性?为什么B+树的节点大小通常设置为操作系统页大小(4KB/16KB)?
追问方向:
你的B+树节点大小是怎么设置的?和BufferPool的Page大小怎么对齐的?
13. 为什么数据库用B+树做索引,而不用你在ACM竞赛里熟悉的红黑树或哈希表?
追问方向:
哈希表在什么场景下比B+树更优?内存数据库(如Redis)和磁盘数据库的选择有什么不同?(这题专门考你理论联系实际)
14. 你的锁管理器支持哪些隔离级别?不同隔离级别下锁的获取和释放策略有什么不同?
追问方向:
Read Committed和Repeatable Read在实现上有什么区别?(RC下读后即放锁,RR下要持有到事务结束)
15. 两阶段锁(2PL)怎么实现的?有没有处理死锁?
追问方向:
死锁检测用的是waits-for graph吗?如果检测到死锁,你选择回滚哪个事务?为什么?(一般回滚代价小的那个)
16. 读写锁(Reader-Writer Lock)在Bustub中怎么用的?读读并发、读写互斥、写写互斥,怎么避免写者饥饿?
追问方向:
你用的是pthread的读写锁还是自己实现的?自己实现的话怎么保证写者优先?
17. 在并发场景下,如何避免幻读(Phantom Read)?
追问方向:
你的锁管理器支持间隙锁(Gap Lock)吗?如果不支持,怎么在RR级别下避免幻读?(或者你们直接支持SERIALIZABLE?)
18. 事务的ACID你是怎么保证的?特别是原子性(Redo/Undo Log)实现了没有?
追问方向(注意简历没提WAL,但可能会问):
你的Bustub有没有实现WAL(Write-Ahead Logging)?如果没有,宕机了怎么恢复?
19. 火山模型(Volcano Model)的核心思想是什么?它的优点和缺点分别是什么?
答:
每个算子都调用Next()虚函数获取一条元组,这种一次一元的拉取模型在大数据量和低延迟场景下有什么性能问题?(虚函数调用开销、指令缓存不友好、CPU分支预测失败——这些都是交易系统非常关注的)
20. 你做的性能优化具体优化了什么?是怎么判断瓶颈在哪里的?
追问方向:
用perf或Valgrind分析过吗?是CPU bound还是Memory bound?
21. 你的查询优化器做了哪些优化?(简历写“根据执行计划进行性能优化”)
追问方向:
有没有做谓词下推(Predicate Pushdown)?索引扫描和全表扫描的选择代价模型是怎么估算的?
22. 对于SELECT * FROM table WHERE indexed_col = ?,执行器怎么决定走索引扫描还是全表扫描?
追问方向:
如果你的索引是B+树,点查走索引的开销是多少?(树高次磁盘I/O)
23. 你在项目中用了RAII管理Page,具体怎么实现的?
追问方向:
PageHandle或PageGuard类是怎么设计的?析构函数里自动Unpin吗?如果复制构造/移动构造处理不当会导致资源重复释放吗?(考C++的Rule of Five)
24. 智能指针在项目中用了哪些?unique_ptr和shared_ptr分别在什么场景用?
追问方向:
B+树节点用unique_ptr管理还是原始指针?为什么?
25. 项目里怎么处理异常安全(Exception Safety)的?比如B+树插入过程中抛出异常怎么办?
追问方向:
你有没有用std::lock_guard来自动管理锁的释放?
26. 你的代码有没有做单元测试?怎么测试并发场景下的正确性?
追问方向(结合简历华为经历):
你在华为做过覆盖率提升,在Bustub里有没有类似的测试?怎么构造并发测试用例来复现死锁或数据竞争?
27. 这个项目大概多少行代码?你在调试过程中用到哪些工具?
追问方向:
GDB怎么调试多线程程序?有没有遇到过内存泄漏,怎么检测的?(AddressSanitizer / Valgrind)
28. Bustub是个磁盘数据库,而我们做的是内存中的低延迟交易系统。如果把Bustub的架构搬到内存场景,哪些模块可以保留,哪些需要完全重写?
答:
BufferPool变成纯内存池、B+树可以保留但节点大小要调小(适配cacheline)、火山模型得换成向量化或编译执行、锁管理器可能改成乐观锁或无锁数据结构。
29. 交易系统中,我们通常避免动态内存分配以减少延迟抖动。你的Bustub项目里哪些地方有动态内存分配?如果让你优化掉它们,你会怎么做?
追问方向:
比如Executor每产生一条元组就new一个对象,你怎么改成对象池或栈上分配?
答:
把内存分配权限上收。具体做法是:Executor 不再自己 new 对象,而是向全局的 QueryMemoryContext 申请内存。这个 Context 在查询开始时创建,结束时整块销毁
30. 你项目里的读写锁在极端并发下性能如何?你觉得无锁编程(Lock-Free)和读写锁在高频交易场景下哪个更合适?
追问方向:
CAS操作了解吗?std::atomic用过吗?内存顺序(memory order)有了解吗?
高频手撕代码题汇总(建议白板/纸上练习)
题目 关联Bustub模块
手写一个简易LRU-K(或LRU)缓存 BufferPool
手写B+树插入(简化版,含分裂逻辑) B+Tree
手写一个读写锁(用互斥锁+条件变量) Lock Manager
手写火山模型的一个算子(如Projection)的Next()实现 Executor
手写shared_ptr的简化实现 RAII Page管理\
1. 这三场比赛(沈阳站、合肥站、亚洲区决赛)你分别担任什么角色?队伍分工是怎样的?
追问方向:
你在队里主要负责码代码、推公式还是读题?决赛银牌和区域赛金牌之间,你觉得差距在哪?
2. 三场比赛打下来,哪一场最有挑战性?遇到了什么问题?
追问方向:
是题目难度、心态崩了、还是某道题卡到最后都没过?最后怎么应对的?
3. 你们队伍三个人是怎么磨合的?有发生过意见分歧吗?
追问方向:
比如一道题两种解法,怎么说服队友?或者队友代码写崩了怎么处理?
4. 金牌题和银牌题大概是什么难度级别?你印象最深的一道题是什么?
追问方向:
用到什么算法/数据结构?当时是怎么想到这个解法的?有没有卡时间/卡内存?
5. 你在竞赛中常用C++ STL,能讲讲std::map和std::unordered_map的底层实现区别吗?什么场景下会优先选std::map?
追问方向:
红黑树的插入删除旋转了解吗?unordered_map的哈希冲突怎么解决?自定义类型作为key要注意什么?(这道题几乎必考,而且和你简历高度相关)
6. 竞赛中怎么处理大规模输入输出的?遇到过卡I/O的情况吗?
追问方向:
ios::sync_with_stdio(false)的原理是什么?自己实现FastIO怎么做的?为什么getchar比cin快?
7. 线段树在你的竞赛中用过吗?能讲讲懒标记的实现吗?区间更新和区间查询的复杂度是多少?
追问方向:
如果数据范围到1e9怎么办(动态开点/离散化)?可持久化线段树了解吗?
8. 动态规划的优化手段你用过哪些?(单调队列优化、斜率优化、四边形不等式?)
追问方向:
挑一个你实际用过的讲讲,什么场景下用,怎么优化复杂度的。
9. B/B+树你项目中用到了,竞赛里用过吗?能说说B+树和红黑树的应用场景区别吗?
追问方向:
为什么数据库用B+树而不是红黑树?磁盘预读和缓存友好怎么理解?
10. ACM比赛的代码一般怎么写?有没有自己的代码模板?里面包含什么?
追问方向:
比如常用的数据结构、快速读入、调试宏定义等。现场有没有因为模板不全吃过亏?
11. 比赛中队友的代码出现bug了,你怎么帮队友debug?
追问方向:
现场有没有具体的例子?怎么定位问题的——加打印、上对拍、还是肉眼走读?
12. 有没有因为罚时策略吃过亏?
追问方向:
你们队的策略是尽早交、保证正确率为主,还是快速拿题为主?WA了之后怎么调整?
13. 竞赛中你写的C代码和工业级C有什么区别?
追问方向:
内存管理(RAII、智能指针)、异常安全、代码可维护性、单元测试这些方面,你觉得竞赛里怎么处理的,和工作中应该有什么不同?
14. 竞赛里你关注时间复杂度是O(n log n)还是O(n²),但在交易系统中我们更关注实际耗时和延迟抖动。你对此怎么看?
追问方向:
比如缓存命中率、分支预测、false sharing这些你了解吗?竞赛经历有没有让你对“性能”有更深的理解?
15. 交易系统要求低延迟,你了解哪些C++性能优化技巧?
追问方向:
内存池、无锁队列、CAS操作、避免动态内存分配、cacheline对齐……这些你接触过吗?
16. 我们系统运行在Linux上,你有多线程编程经验吗?
追问方向:
竞赛里可能不多,但可以问问理论:互斥锁和自旋锁的区别、读写锁适用场景、ABA问题等。
17. Python和Shell的掌握程度怎么样?
追问方向:
竞赛里可能用Python写对拍脚本?或者Shell做数据预处理?有没有实际写过的例子?
18. 你提到单元测试覆盖率从60%提升到75%,这是在你华为的工作中做的。在竞赛场景下你做过类似测试吗?
追问方向:
竞赛里怎么验证代码正确性?(对拍、构造极端数据、随机数据)和单元测试的理念有什么异同?
高频手撕代码题汇总(建议白板/纸上练习)
题目 考察点
手写一个线程安全的单例模式(C11版本) 多线程 + C11特性
vector扩容机制 STL底层 + 性能理解
智能指针(shared_ptr/unique_ptr/weak_ptr)的实现原理 RAII + 引用计数
进程和线程的区别 / 协程了解吗 操作系统基础
TCP三次握手 / 粘包问题怎么解决 网络编程基础
设计一个无锁队列 并发 + CAS