Momenta C++ 智驾 二面 面经

1. 自我介绍,介绍一下你觉得最有挑战性的项目

2. 你提到用过多任务架构,任务数量多了之后内存压力怎么处理的?静态分配和动态分配你怎么选择?

答:任务多了之后内存压力主要来自两块:每个任务的栈空间,以及任务间通信用的队列和信号量。

栈空间的处理:

  • 先用 uxTaskGetStackHighWaterMark 跑一段时间,看每个任务实际用了多少栈,按实际用量加一定余量分配,不要每个任务都给一个很大的默认值
  • 对于简单任务(只做状态机跳转、没有深层函数调用)可以给很小的栈,比如 128 字
  • 对于有 printf、sprintf 或者复杂字符串处理的任务,栈需要大一些

静态分配 vs 动态分配:

  • 嵌入式项目里我倾向于全部用静态分配,xTaskCreateStaticxSemaphoreCreateMutexStatic 这些接口
  • 原因是动态分配在运行时可能失败,而且堆碎片化问题在长时间运行的设备上会越来越严重
  • 静态分配在编译期就能知道内存布局,链接器会报错如果超出 RAM 限制,比运行时崩溃好排查得多
  • 动态分配适合原型阶段快速开发,量产前最好改成静态

3. 讲一下 C++ 的内存模型,std::atomic 的几种内存序分别是什么含义?

答:C++ 内存模型定义了多线程程序中内存操作的可见性和顺序规则。

六种内存序:

memory_order_relaxed

  • 只保证原子性,不保证顺序
  • 不阻止编译器和 CPU 重排
  • 适合只需要原子性的计数器,比如统计次数,不关心和其他操作的顺序关系

memory_order_acquire

  • 用于读操作,保证该操作之后的所有读写不会被重排到该操作之前
  • 配合 release 使用,保证看到 release 写入的值之后,也能看到 release 之前的所有写入

memory_order_release

  • 用于写操作,保证该操作之前的所有读写不会被重排到该操作之后
  • 典型用法:写完数据后 release 写一个标志,另一个线程 acquire 读到标志后能看到完整数据

memory_order_acq_rel

  • 同时具有 acquire 和 release 语义,用于读改写操作(fetch_add 等)

memory_order_seq_cst

  • 最强,所有线程看到的操作顺序完全一致
  • 默认值,性能开销最大,在 ARM 等弱内存序架构上会插入内存屏障指令

memory_order_consume

  • 比 acquire 弱,只保证依赖链上的顺序,实践中编译器通常把它当 acquire 处理,基本不用

实际使用:

std::atomic<bool> ready{false};
std::string data;

// 生产者
data = "hello";
ready.store(true, std::memory_order_release); // 保证 data 写入在 ready 之前

// 消费者
while (!ready.load(std::memory_order_acquire)); // 看到 true 后能看到完整的 data
std::cout << data;

4. 手写题:实现一个无锁的单生产者单消费者队列

答:

#include <atomic>
#include <vector>
#include <optional>

template<typename T>
class SPSCQueue {
public:
    explicit SPSCQueue(size_t capacity)
        : capacity_(capacity + 1), // 多一个槽位区分满和空
          buffer_(capacity + 1),
          head_(0),
          tail_(0) {}

    // 生产者调用,队列满返回 false
    bool push(T value) {
        size_t tail = tail_.load(std::memory_order_relaxed);
        size_t next = (tail + 1) % capacity_;

        // 队列满:下一个位置是 head
        if (next == head_.load(std::memory_order_acquire)) {
            return false;
        }

        buffer_[tail] = std::move(value);
        tail_.store(next, std::memory_order_release);
        return true;
    }

    // 消费者调用,队列空返回空
    std::optional<T> pop() {
        size_t head = head_.load(std::memory_order_relaxed);

        // 队列空:head == tail
        if (head == tail_.load(std::memory_order_acquire)) {
            return std::nullopt;
        }

        T value = std::move(buffer_[head]);
        head_.store((head + 1) % capacity_, std::memory_order_release);
        return value;
    }

    bool empty() const {
        return head_.load(std::memory_order_acquire) ==
               tail_.load(std::memory_order_acquire);
    }

private:
    const size_t capacity_;
    std::vector<T> buffer_;
    // 两个指针分别由生产者和消费者独占写,避免伪共享
    alignas(64) std::atomic<size_t> head_; // 消费者写
    alignas(64) std::atomic<size_t> tail_; // 生产者写
};

关键点:

  • head_ 只有消费者写,tail_ 只有生产者写,不需要锁
  • alignas(64) 让两个原子变量在不同缓存行,避免伪共享(false sharing)导致性能下降
  • push 里读 head 用 acquire,写 tail 用 release;pop 里读 tail 用 acquire,写 head 用 release
  • 容量加一是为了用空槽位区分队列满和队列空的状态

5. 虚函数在性能敏感场景下有什么替代方案?

答:虚函数的开销:间接寻址(通过 vptr 找 vtable 再找函数)、无法内联、可能导致 icache miss。

替代方案:

CRTP(奇异递归模板模式):

template<typename Derived>
class Base {
public:
    void process() {
        static_cast<Derived*>(this)->processImpl(); // 编译期确定,可内联
    }
};

class Derived : public Base<Derived> {
public:
    void processImpl() { /* 具体实现 */ }
};

  • 编译期多态,零运行时开销,可以内联
  • 缺点:不能把不同类型放在同一个容器里,失去运行时多态能力

std::variant + std::visit

using Shape = std::variant<Circle, Rectangle, Triangle>;
std::vector<Shape> shapes;

for (auto& s : shapes) {
    std::visit([](auto& shape) { shape.draw(); }, s);
}

  • 编译期生成跳转表,比虚函数快
  • 类型集合固定,不能运行时扩展

函数指针 / std::function

  • 函数指针开销和虚函数类似,但没有 vtable 查找
  • std::function 有额外的类型擦除开销,比虚函数还慢,不推荐热路径使用

实际选择:

  • 自动驾驶感知模块的热路径(每帧处理)用 CRTP 或直接模板
  • 插件系统、策略模式等需要

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

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

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

全部评论

相关推荐

钱嘛数字而已:拖拉机被发明出来之后,就不需要农民了吗?农民还是需要的,但不需要这么多了,另外对农民的要求也变高了,需要会开拖拉机。
点赞 评论 收藏
分享
03-24 02:07
已编辑
南开大学 Java
全程2小时共享屏幕+看项目具体代码,压力面算法题(40min+20min优化):74.&nbsp;搜索二维矩阵&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;秒了1482.&nbsp;制作&nbsp;m&nbsp;束花所需的最少天数贪心+二分&nbsp;&nbsp;&nbsp;&nbsp;搞了半天,不过撕出来了问怎么优化时间复杂度1.&nbsp;TCP连接建立过程为何是3次segment交互,而非4次?请详细说明四次握手合并为三次的核心原因。2.&nbsp;没见过的代码,让我猜结果,后来查了是js`const&nbsp;a&nbsp;=&nbsp;{i:1,toString:&nbsp;function(){return&nbsp;a.i++;}};console.log(a==1&nbsp;&amp;&amp;&nbsp;a==2&nbsp;&amp;&amp;&nbsp;a==3)`为何会输出&nbsp;`true`?其底层类型转换和执行顺序是怎样的?3.&nbsp;请解释CPU执行指令时,为何数字比较是串行执行而非并行处理?这和`a==1&nbsp;&amp;&amp;&nbsp;a==2&nbsp;&amp;&amp;&nbsp;a==3`的执行逻辑有何关联?4.&nbsp;大模型生成语句基于HTTP长连接逻辑时,和WebSocket的全双工通信特性有何本质区别?5.&nbsp;SSE是否具备双工通信能力?如何清晰澄清SSE的单向推送特性?6.&nbsp;SSE实现客户端消息推送时,持续推送无法中断的问题该如何解决?是否需要后端配合实现流程控制?7.&nbsp;SSE存在自动断连风险,该如何优化协议稳定性以适配业务场景?有哪些重连或保活方案?8.&nbsp;大模型流式输出的分段内容格式该如何设计?需要考虑哪些兼容性和可读性要求?9.&nbsp;智能体Prompt的完整构建流程是怎样的?从角色设定、任务范围到格式化输出要求,具体步骤是什么?10.&nbsp;智能体的场景化细节设计有哪些?该如何嵌入Prompt?11.&nbsp;AI&nbsp;Agent的核心工作流程是什么?请详细说明从用户需求分析、工具调用意图生成,到参数转化、MCP客户端校验执行的全链路。12.&nbsp;MCP调用逻辑的权限归属问题是什么?谁来主导MCP工具的调用?13.&nbsp;LangChain在你的项目中具体承担什么角色?是否仅作为大模型接口?如何实现框架的深度定制?14.&nbsp;大模型在Agent系统中是思考核心,那工具调用的触发主体是谁?是Agent解析字符串触发,还是大模型主动分析意图后调用?15.&nbsp;大语言模型输出字符串的机制是什么?工具调用的触发时机具体在哪个环节?16.&nbsp;Agent与大模型的协作流程是怎样的?17.&nbsp;MCP调用工具时的参数校验流程是怎样的?校验失败后该如何处理异常?18.&nbsp;MCP和Skill的功能边界是什么?两者在工具调用、模块化设计上有何区别?19.&nbsp;如何将现有MCP工具改造为支持Skill功能?具体的代码或配置修改步骤是什么?20.&nbsp;渐进式披露技术的具体实现方式是什么?如何通过文件格式和系统提示词控制大模型读取范围?21.&nbsp;改造工具调用链路(如从MCP切换到skill接口)时,是否需要修改大模型本身?如何实现解耦以避免核心代码变动?22.&nbsp;新建Scale工具文件并注册到映射体系的具体操作步骤是什么?如何保证工具调用的灵活性?23.&nbsp;直接使用AI生成的代码方案(如Markdown表格形式的函数列表)是否可靠?存在哪些灵活性不足的问题?24.&nbsp;现有文件名匹配逻辑不够完善,该如何优化?需要考虑哪些匹配规则和异常场景?25.&nbsp;项目中的主控逻辑和记忆模块是否为自主实现?LangChain之外还使用了哪些技术栈?反问:一周出结果
冰炸橙汁_不做oj版:已吓哭
发面经攒人品
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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