文章

C++/Go/AI 混合技术栈:30 道高频追问 + 参考回答

面向 C++、Golang 和 AI 工程化的 30 道高频面试题,从基础概念到工程实践,附详细追问思路和参考回答。

C++/Go/AI 混合技术栈:30 道高频追问 + 参考回答

面试 C++/Go/AI 混合技术栈岗位时,面试官往往会追问一些跨领域的问题。本文整理了 30 道高频追问,涵盖现代 C++、Golang 高并发、AI 工程化三个维度,帮助你系统准备。


第一部分:现代 C++(10 道)

Q1: std::unique_ptrstd::shared_ptr 的核心区别是什么?什么场景下不能用 shared_ptr

参考回答:

  • unique_ptr 独占所有权,移动语义,零开销;shared_ptr 共享所有权,引用计数,有原子操作开销
  • 不能用 shared_ptr 的场景:需要严格所有权控制、性能敏感路径、可能存在循环引用但未使用 weak_ptr 的情况

追问: 如果需要在回调中保持对象生命周期,应该用哪种?

  • 答:shared_ptr,但要警惕生命周期延长导致的延迟析构问题

Q2: 完美转发(Perfect Forwarding)解决了什么问题?std::forwardstd::move 的区别?

参考回答:

  • 完美转发解决的是:将参数原样转发给另一个函数,保持值类别(左值/右值)和 const 性
  • std::move 无条件转为右值;std::forward 按模板参数条件转发

追问: 万能引用(Universal Reference)是什么?T&& 在什么情况下是右值引用,什么情况下是万能引用?

  • 答:类型推导语境下的 T&& 是万能引用,具体类型(如 int&&)是右值引用

Q3: C++11 的内存模型中,memory_order 有哪些级别?seq_cstacquire/release 的区别?

参考回答:

  • 6 个级别:relaxed, consume, acquire, release, acq_rel, seq_cst
  • seq_cst 是最强一致性,保证全局顺序;acquire/release 是单向同步,性能更好但只在配对时保证可见性

追问: 什么情况下可以用 relaxed?举具体例子。

  • 答:纯计数器场景(如统计请求数),只要求原子性不要求顺序性

Q4: volatileatomic 的区别?为什么 volatile 不能保证线程安全?

参考回答:

  • volatile 只禁止编译器优化,保证每次都从内存读取,但不保证 CPU 指令重排序
  • atomic 提供内存序保证和原子操作,是线程安全的正确工具

追问:volatile 还有什么用?

  • 答:内存映射 I/O、信号处理程序中的标志位(配合 sig_atomic_t

Q5: 解释虚函数表的内存布局。多重继承时虚表指针怎么处理?

参考回答:

  • 每个有虚函数的类有一个虚表(vtable),对象包含虚表指针(vptr)
  • 多重继承时,对象可能有多个 vptr,每个基类子对象一个,派生类有自己的虚表,同时调整 this 指针

追问: 菱形继承的虚表布局?

  • 答:虚继承时,虚基类在对象最末尾,通过虚基类指针(vbptr)访问

Q6: 为什么模板代码通常放在头文件中?显式实例化(Explicit Instantiation)是什么?

参考回答:

  • 模板是编译期代码生成,需要看到完整定义才能实例化
  • 显式实例化是在 .cpp 中用 template class Foo<int> 预先生成代码,减少编译时间和二进制大小

追问: 模板编译错误为什么那么长?

  • 答:SFINAE 机制导致编译器尝试大量重载,错误信息逐层展开

Q7: std::vector 扩容策略是什么?为什么是 2 倍(或 1.5 倍)?

参考回答:

  • 通常是 2 倍(GCC)或 1.5 倍(VC++),需要连续内存,扩容时申请新空间、移动元素、释放旧空间
  • 2 倍保证摊还 O(1),但可能浪费内存;1.5 倍允许复用之前释放的内存(黄金分割)

追问: 如何避免频繁扩容?

  • 答:reserve 预分配,shrink_to_fit 释放多余空间

Q8: RAII 是什么?如何用 RAII 管理非内存资源(如文件句柄、锁)?

参考回答:

  • RAII(Resource Acquisition Is Initialization):资源在构造时获取,析构时释放
  • 自定义 RAII 类:构造函数获取资源,析构函数释放,配合智能指针或栈对象使用

追问: std::lock_guardstd::unique_lock 的区别?

  • 答:lock_guard 简单不可复制,构造加锁析构解锁;unique_lock 可延迟加锁、解锁、转移所有权

Q9: 解释 CRTP(Curiously Recurring Template Pattern)及其应用场景。

参考回答:

  • CRTP:派生类将自己作为模板参数传给基类,如 class Derived : public Base<Derived>
  • 用途:静态多态(避免虚函数开销)、代码复用、编译期接口检查

追问: CRTP 和虚函数的性能对比?

  • 答:CRTP 编译期绑定,内联优化更好,无 vtable 开销,但代码膨胀

Q10: 如何检测和解决内存泄漏?Valgrind、ASan、LSan 的区别?

参考回答:

  • Valgrind:模拟执行,慢但准确,不需要重编译
  • ASan(AddressSanitizer):编译期插桩,检测 use-after-free、heap-buffer-overflow,约 2x 开销
  • LSan(LeakSanitizer):ASan 的一部分,专用于检测泄漏

追问: 生产环境用什么?

  • 答:一般不用,用采样 profiling(如 tcmalloc 的 heap profiler)+ 监控告警

第二部分:Golang 高并发(10 道)

Q11: Goroutine 的调度模型是什么?M、P、G 分别代表什么?

参考回答:

  • G(Goroutine):用户态轻量线程,~2KB 栈,动态增长
  • M(Machine):OS 线程,执行 G 的载体
  • P(Processor):逻辑处理器,持有 G 队列,P 的数量决定并行度(默认 GOMAXPROCS)

追问: 如果某个 G 阻塞了系统调用,会发生什么?

  • 答:M 和 G 一起阻塞,P 被 detach 去绑定新的 M 继续调度其他 G

Q12: Channel 的底层实现是什么?有缓冲和无缓冲的区别?

参考回答:

  • 底层:环形队列 + 锁 + 发送/接收等待队列
  • 无缓冲:同步通信,发送和接收必须同时准备好,否则阻塞
  • 有缓冲:异步通信,缓冲满时发送阻塞,空时接收阻塞

追问: 怎么实现一个 “try send”(非阻塞发送)?

  • 答:用 selectdefault 分支

Q13: sync.Mutexsync.RWMutex 的使用场景?锁粒度怎么控制?

参考回答:

  • Mutex:读写都互斥,简单场景
  • RWMutex:读多写少,读不互斥,写互斥
  • 粒度控制:细粒度锁减少竞争但增加复杂度,粗粒度锁简单但可能成为瓶颈

追问: 什么是锁的 “粒度过细” 问题?

  • 答:频繁加解锁开销、死锁风险增加、代码复杂度上升

Q14: Go 的 GC 算法是什么?三色标记法的流程?写屏障的作用?

参考回答:

  • GC:并发三色标记-清除 + 混合写屏障
  • 三色:白(待回收)、灰(处理中)、黑(存活)
  • 流程:根节点扫描(STW 很短)→ 并发标记 → 重新扫描(STW)→ 并发清除
  • 写屏障:记录指针变化,防止漏标,Go 1.8 后是混合写屏障(插入+删除)

追问: 为什么 Go 没有分代 GC?

  • 答:Goroutine 栈小且动态伸缩,分代收益不明显,且写屏障复杂度增加

Q15: context.Context 的设计意图?如何正确传递和取消?

参考回答:

  • 意图:跨 API 边界和 Goroutine 传递截止时间、取消信号、请求范围值
  • 正确用法:函数第一个参数传 ctx,不存储在结构体中,用 WithCancel/WithTimeout/WithDeadline 派生

追问: context.Value 的合理使用场景?

  • 答:请求追踪 ID、认证信息,避免用于传递业务参数

Q16: 如何用 Go 实现一个协程池?限制并发数的方法?

参考回答:

  • 协程池:Worker 模式,固定数量的 worker 从 channel 取任务处理
  • 限制并发:使用 buffered channel 作为信号量,sem <- struct{}{} 获取,<-sem 释放

追问: errgroupsync.WaitGroup 的区别?

  • 答:errgroup 包装了 WaitGroup+Context,可取消和收集错误,更适合任务编排

Q17: Go 的 defer 执行顺序?defer 在返回值上的坑?

参考回答:

  • 顺序:LIFO(后进先出),多个 defer 按定义顺序逆序执行
  • 返回值坑:命名返回值时,defer 可以修改;非命名返回值不能修改

追问:

1
2
3
4
func f() (r int) {
    defer func() { r++ }()
    return 1
}

返回什么?

  • 答:2,因为 return 1 先赋值给 r,然后执行 deferr++

Q18: Go 的接口(Interface)是如何实现的?动态派发和静态派发的区别?

参考回答:

  • 接口实现:结构体包含类型指针和数据指针(iface 或 eface),运行时检查类型元数据
  • 动态派发:通过接口调用方法,运行时查表;静态派发:直接调用,编译期确定

追问: 空接口 interface{}any 的区别?

  • 答:Go 1.18+ anyinterface{} 的别名,完全等价

Q19: Go 的内存分配器(TCMalloc)原理?堆和栈的区别?逃逸分析是什么?

参考回答:

  • TCMalloc:线程缓存(mcache)→ 中心缓存(mcentral)→ 全局堆(mheap),减少锁竞争
  • 栈:函数调用帧,自动分配回收;堆:动态分配,GC 管理
  • 逃逸分析:编译器决定变量放栈还是堆,如返回指针、闭包引用、大对象会逃逸到堆

追问: 如何查看逃逸分析结果?

  • 答:go build -gcflags="-m"

Q20: 如何设计一个高性能的 Go 服务?从连接管理、限流、超时角度分析。

参考回答:

  • 连接管理:HTTP Keep-Alive、连接池、优雅关闭(http.Server.Shutdown
  • 限流:令牌桶(golang.org/x/time/rate)、漏桶、分布式限流(Redis)
  • 超时:每层都设超时(连接、读写、业务),用 context 传播

追问: 什么是 “Goroutine 泄漏”?如何检测?

  • 答:Goroutine 永久阻塞无法退出,用 pprof 的 goroutine profile 检测

第三部分:AI 工程化(10 道)

Q21: 什么是 MCP(Model Context Protocol)?它解决了什么问题?

参考回答:

  • MCP:Anthropic 提出的开放协议,标准化 LLM 与外部工具/数据的交互方式
  • 解决问题:替代碎片化的 Function Calling 实现,一次集成到处使用,工具生态标准化

追问: MCP 和传统的 Function Calling 有什么区别?

  • 答:Function Calling 是各模型厂商的 API 格式;MCP 是协议层,模型无关,工具可复用

Q22: AI Agent 的核心架构是什么?ReAct、Plan-and-Execute 的区别?

参考回答:

  • 核心架构:感知(Perception)→ 推理(Reasoning)→ 行动(Action)→ 记忆(Memory)
  • ReAct:Thought → Action → Observation 循环,适合探索性任务
  • Plan-and-Execute:先规划再执行,适合确定性任务

追问: 什么时候应该用 ReAct,什么时候用 Plan-and-Execute?

  • 答:信息不足需要探索用 ReAct;步骤明确、需要效率用 Plan-and-Execute

Q23: 什么是 RAG(Retrieval-Augmented Generation)?完整的 RAG 链路包含哪些环节?

参考回答:

  • RAG:检索增强生成,结合向量检索和 LLM 生成,解决知识时效性和幻觉问题
  • 链路:文档加载 → 切分(Chunking)→ 向量化(Embedding)→ 索引存储 → 检索(相似度搜索)→ 重排序 → 生成

追问: 如何优化 RAG 的检索质量?

  • 答:混合检索(向量+关键词)、查询重写、重排序(Rerank)、元数据过滤

Q24: LLM 的 “幻觉”(Hallucination)是什么?如何缓解?

参考回答:

  • 幻觉:模型生成看似合理但实际错误或不存在的信息
  • 缓解方法:RAG 提供上下文、Fact-checking、Self-consistency 采样、多 Agent 验证、训练阶段 RLHF

追问: Self-consistency 具体怎么做?

  • 答:多次采样取多数投票,或让模型自我验证(”请检查以上回答的准确性”)

Q25: Prompt Engineering 的核心技巧有哪些?CoT、ToT、Few-shot 的区别?

参考回答:

  • Few-shot:给示例教模型任务格式
  • CoT(Chain-of-Thought):要求模型”一步步思考”,显式推理过程
  • ToT(Tree-of-Thoughts):维护多个推理路径,评估后选择最佳

追问: 什么是 “Zero-shot CoT”?

  • 答:不给示例,只在 Prompt 里加 “Let’s think step by step” 触发推理

Q26: 如何评估 LLM 应用的效果?除了 Perplexity 还有什么指标?

参考回答:

  • 生成质量:BLEU、ROUGE(对比参考)、人工评估
  • 事实性:FactScore、引用准确率
  • 任务完成:准确率、F1、用户满意度
  • 效率:延迟、吞吐量、成本

追问: RAG 系统怎么评估检索质量?

  • 答:Hit Rate、MRR(Mean Reciprocal Rank)、NDCG、人工相关性判断

Q27: 什么是 LLM 的 “上下文窗口”(Context Window)?长上下文模型有什么挑战?

参考回答:

  • 上下文窗口:模型单次能处理的最大 token 数(如 4K、32K、128K、1M)
  • 挑战:注意力计算 O(n²) 复杂度、KV Cache 显存占用、长距离依赖捕捉、”Lost in the Middle” 现象

追问: 如何处理超过上下文窗口的文档?

  • 答:Map-Reduce(分段处理再聚合)、RAG 检索相关片段、层次化摘要

Q28: AI 应用的部署模式有哪些?从成本和延迟角度如何选择?

参考回答:

  • 部署模式:云服务 API(OpenAI/Claude)、私有化部署(vLLM/TGI)、边缘部署(ONNX/TensorRT)
  • 成本角度:高频用自托管,低频用 API;延迟角度:边缘部署 < 私有化 < 云服务

追问: vLLM 的 PagedAttention 是什么?

  • 答:将 KV Cache 分页管理,减少显存碎片,提高吞吐

Q29: 如何将 AI 能力集成到现有系统?从架构设计角度考虑哪些因素?

参考回答:

  • 架构因素:
    • 解耦:AI 服务作为独立微服务
    • 容错:降级策略、超时控制、重试机制
    • 可观测性:输入输出日志、Token 消耗监控
    • 成本控制:缓存、批量处理、模型路由(大小模型协同)

追问: 什么是 “模型路由”(Model Routing)?

  • 答:简单请求用小模型(快/便宜),复杂请求路由到大模型(准/贵)

Q30: 传统软件工程与 AI 辅助编程的工作流有什么不同?如何保持代码质量?

参考回答:

  • 不同点:
    • 传统:人写代码 → 编译 → 测试 → 部署
    • AI 辅助:人写 Prompt → AI 生成 → 人审查 → 测试 → 部署
  • 质量保证:
    • 严格的 Code Review,特别是 AI 生成代码
    • 自动化测试覆盖(单元测试、集成测试)
    • 类型系统和静态分析
    • 安全扫描(AI 可能生成有漏洞的代码)

追问: 如何防止 AI 生成代码中的安全漏洞?

  • 答:静态安全扫描(SAST)、依赖审计、沙箱测试、人工安全审查

总结

这 30 道题覆盖了 C++/Go/AI 混合技术栈的核心知识点。面试准备建议:

  1. C++ 重点:现代 C++11/14/17 特性、内存模型、并发原语、性能优化
  2. Go 重点:Goroutine 调度、Channel 通信、Context 传播、GC 原理
  3. AI 重点:MCP/Agent 架构、RAG 链路、Prompt Engineering、生产化部署

实际面试中,面试官往往会跨领域追问,比如:

  • “你用 C++ 做过高性能服务,如果现在要接入 AI 能力,怎么设计?”
  • “Go 的并发模型和 AI Agent 的任务编排有什么异同?”

建议准备 1-2 个完整项目,能串联起这三个技术栈的应用场景。

本文由作者按照 CC BY 4.0 进行授权