小米 C++ 二面 攒人品了

二面明显比一面难度上了一个台阶,面试官是组内 senior,开场直接问项目深挖,每个技术决策都要说清楚为什么这么做、有没有更好的方案。八股比例少了,更多是系统设计和场景题,考察你有没有真正在工程里踩过坑。代码题也更偏综合,不是纯算法,而是结合实际场景。整体感受是:说不知道比瞎说好,面试官很容易听出来你是背的还是真懂的。

1. 自我介绍 + 项目深挖

略(重点准备:项目中遇到的性能瓶颈、你做了什么优化、结果如何量化)

2. C++ 内存模型中的 memory order 有哪些?memory_order_relaxedmemory_order_seq_cst 的区别?

C++11 定义了六种内存序:

relaxed

只保证原子性,不保证顺序

consume

依赖链上的读操作有序(实践中很少用)

acquire

当前线程后续读写不会重排到此操作之前

release

当前线程之前的读写不会重排到此操作之后

acq_rel

acquire + release

seq_cst

全局顺序一致,最强,也最慢

std::atomic<int> x{0};

// relaxed:只保证 x 的读写是原子的,不管顺序
x.store(1, std::memory_order_relaxed);

// seq_cst:所有线程看到的操作顺序一致,有全局内存屏障
x.store(1, std::memory_order_seq_cst);

典型用法:生产者用 release 写数据,消费者用 acquire 读,配对使用保证可见性,比 seq_cst 性能好。

3. 说说 C++ 对象的生命周期和 RAII,你在项目里怎么用的?

RAII(Resource Acquisition Is Initialization):资源在构造时获取,在析构时释放,利用栈对象生命周期自动管理资源,避免泄漏。

标准库里的 RAII 例子:unique_ptrlock_guardfstreamvector

自定义 RAII:

class FileHandle {
    FILE* fp_;
public:
    explicit FileHandle(const char* path) {
        fp_ = fopen(path, "r");
        if (!fp_) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (fp_) fclose(fp_); }

    // 禁止拷贝,允许移动
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FileHandle(FileHandle&& o) noexcept : fp_(o.fp_) { o.fp_ = nullptr; }
};

关键点:析构函数不能抛异常(noexcept),移动时要把源对象置空,防止 double free。

4. 模板元编程中 std::enable_ifif constexpr 的区别?各自适用场景?

std::enable_if 基于 SFINAE,在编译期根据条件启用/禁用函数重载:

template<typename T>
std::enable_if_t<std::is_integral_v<T>, void>
process(T val) { /* 整数版本 */ }

if constexpr(C++17)在函数体内做编译期分支,更直观:

template<typename T>
void process(T val) {
    if constexpr (std::is_integral_v<T>) {
        // 整数分支,另一分支不参与编译
    } else {
        // 其他类型分支
    }
}

区别:enable_if 用于控制重载集合,if constexpr 用于函数体内分支。C++17 后优先用 if constexpr,代码更清晰。

5. 什么是协程?C++20 的协程和线程有什么本质区别?

协程是可以暂停和恢复执行的函数,暂停时不阻塞线程,控制权交回调用方。

与线程的本质区别:

  • 线程是 OS 调度的,切换有内核开销(上下文切换约 1~10μs)
  • 协程是用户态调度的,切换只是函数调用级别的开销(纳秒级)
  • 线程是抢占式的,协程是协作式的(主动 co_await 让出)
  • 一个线程可以跑成千上万个协程

C++20 协程三个关键字:co_awaitco_yieldco_return。适合 IO 密集型场景,比如网络服务器,避免为每个连接创建一个线程。

6. 说说 std::deque 的底层结构,为什么它的迭代器比 vector 复杂?

deque 底层是分段连续内存:维护一个指针数组(map),每个指针指向一块固定大小的内存块(chunk)。

map: [ptr0] [ptr1] [ptr2] ...
       |       |       |
     chunk0  chunk1  chunk2

这样头尾插入都是 O(1),不需要整体搬移。

迭代器复杂的原因:迭代器需要同时维护当前元素指针、当前 chunk 的首尾边界、以及 map 中的位置,++ 操作需要判断是否跨 chunk。而 vector 迭代器就是一个裸指针,++ 直接加一。

7. 一个 C++ 程序从源码到运行,经历了哪些阶段?动态链接和静态链接的区别?

编译流程:

  1. 预处理(cpp):展开宏、include,生成 .i
  2. 编译(cc1):生成汇编 .s
  3. 汇编(as):生成目标文件 .o
  4. 链接(ld):合并 .o,解析符号,生成可执行文件

静态链接:库代码直接打包进可执行文件,体积大,无运行时依赖,部署简单。

动态链接:可执行文件只记录库名和符号,运行时由动态链接器(ld.so)加载 .so/.dll。多个进程共享同一份库的内存映射,节省内存,但有版本依赖问题(DLL hell)。

8. 说说 Linux 的虚拟内存布局,栈和堆的增长方向?mmap 映射在哪里?

典型 64 位 Linux 进程地址空间(从低到高):

0x0000...  保留(NULL 陷阱)
           代码段(text)
           数据段(data/bss)
           堆(向高地址增长)↑
           ...
           mmap 区域(共享库、匿名映射)
           ...
           栈(向低地址增长)↓
0xFFFF...  内核空间

  • 堆:brk/sbrkmmap 分配,向高地址增长
  • 栈:向低地址增长,默认大小通常 8MB(ulimit

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

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

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

全部评论
这是基本功吗
点赞 回复 分享
发布于 昨天 22:22 香港

相关推荐

评论
1
3
分享

创作者周榜

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