面灵AI→

元绎互动 C++ 面经:智能指针、内存管理与并发

时间
2026-09
来源
牛客网

《面试题目》

  1. 智能指针有了解吗?shared_ptr 是线程安全的吗?
  2. new/delete 和 malloc/free 有什么区别?
  3. C++ 程序运行时的内存是如何管理的?
  4. 堆和栈有什么区别?函数调用时栈空间是如何布局的?
  5. move 有了解吗?
  6. 模板有了解吗?模板的类型是在什么时候确定的?相关内存优化了解吗?
  7. 了解哪些设计模式?SOLID 原则是什么?
  8. 多线程之间有哪些同步方式?C++ 中如何进行原子操作?
  9. 对于锁都了解哪些?用过哪些?读写锁有了解吗?
  10. 哈希表是如何实现的?哈希冲突有哪些解决方法?
  11. Reactor 有了解吗?

《参考解析》

shared_ptr 的线程安全要分三层说

第一层,引用计数的增减是原子的(控制块里的计数用原子操作或锁保护),所以多个线程各自持有一份 shared_ptr 副本、各自拷贝或析构,是安全的。第二层,同一个 shared_ptr 实例被多个线程同时读写(一个线程赋值、另一个线程读)不是线程安全的,因为指针和控制块指针这两个字段不是一起原子更新的,需要加锁或用 C++20 的 std::atomic<std::shared_ptr<T>>。第三层,被指向的对象本身的并发访问,shared_ptr 完全不负责。另外要提循环引用——两个对象互相持有 shared_ptr 会导致引用计数永不归零,用 weak_ptr 打破;以及 make_shared 把对象和控制块合并成一次分配,缓存更友好但会让 weak_ptr 延长整块内存的生命周期。

new/delete 与 malloc/free

new 做两件事:调用 operator new 分配原始内存,然后在上面构造对象;delete 先析构再释放内存。malloc/free 只管字节,不调用构造与析构。其他差异:new 返回正确类型的指针、失败抛 bad_alloc(malloc 返回 NULL);new[] 必须配 delete[],混用是未定义行为;malloc 家族有 realloc,new 没有;operator new 可以重载,用来做内存池、对齐分配或统计。自定义类型上混用 free 释放 new 出来的对象,析构不会执行,是典型的内存泄漏加未定义行为。

进程内存布局与函数栈帧

从低地址到高地址大致是:代码段(text)、只读数据(rodata)、已初始化全局与静态数据(data)、未初始化数据(bss)、堆(向上增长,小分配走 brk、大分配走 mmap)、内存映射区(共享库、文件映射)、栈(向下增长)、内核空间。函数调用时栈帧里依次放:调用者的参数与寄存器溢出区、返回地址、保存的 rbp、被调用者的局部变量与临时对象、对齐填充。栈默认大小通常 8 MB(ulimit -s 可查),递归过深或超大局部数组会栈溢出;堆由分配器管理(glibc 的 arena、tcache、bins),释放后不一定立刻还给操作系统。

move 语义

std::move 本身不移动任何东西,它只是一个到右值引用的类型转换。真正干活的是移动构造和移动赋值:把源对象的资源指针接管过来并把源置为有效但未指定状态。移动构造标 noexcept 很重要,因为 vector 扩容时只有在移动构造不抛异常的情况下才会选择移动而不是拷贝。C++17 起有保证的拷贝消除,return localObj; 直接构造在返回槽位,所以不要写 return std::move(localObj);,那反而会抑制 RVO。

模板的类型在什么时候确定

编译期。模板是编译期多态,实例化发生在编译时,所以类型错误在编译期就能发现,代价是编译时间和代码膨胀。理解要点:两阶段名字查找(非依赖名在定义时查找,依赖名在实例化时通过 ADL 查找);相关优化包括用 if constexpr 在编译期剪枝、用 CRTP 做静态多态省掉虚函数开销、模板特化与 traits 做零成本抽象。反面是类型擦除(std::function、虚函数)会带来间接调用开销,选型时看是否需要运行期多态。

设计模式与 SOLID

SOLID 五条要能各举一个例子:单一职责(一个类只干一件事)、开闭(新增功能靠扩展而不是改旧代码)、里氏替换(子类不能破坏父类的契约,别让 Square 继承 Rectangle)、接口隔离(别让实现类实现用不到的方法)、依赖倒置(依赖抽象而不是具体实现,便于替换和测试)。C++ 里最常用的其实是 RAII 加智能指针、工厂、策略、观察者、适配器、装饰器、单例。说模式时带上代价:单例让测试变难,观察者要处理生命周期和线程安全,装饰器会引入多层间接。

多线程同步与原子操作

同步手段分几类:互斥量(mutex、recursive_mutex、shared_mutex)、条件变量(配 mutex 用的等待与通知)、信号量(C++20 的 counting_semaphore)、原子类型(std::atomic)、以及无锁结构里的内存序。原子操作要讲清 compare_exchange_weak 与 strong 的区别(weak 允许伪失败,适合放在循环里)、常用内存序的语义(relaxed 只保证原子性、acquire/release 配对建立同步关系、seq_cst 是默认的全局顺序)以及 fetch_add、load/store 的用法。还要提伪共享:两个线程写同一缓存行里的不同变量会让缓存行反复失效,用 alignas(64) 填充隔开。读写锁适合读多写少,但要注意写者饥饿,而且它不可重入。

哈希表的实现与冲突解决

结构是数组加哈希函数。冲突解决两大类:链地址法(每个桶挂链表,Java 8 的 HashMap 在链表长度达到 8 且容量不小于 64 时转红黑树;C++ 的 unordered_map 也是桶加链表)和开放寻址(线性探测、二次探测、双重散列;删除需要墓碑标记,否则会截断探测链)。配套要有负载因子和扩容 rehash 策略,扩容时渐进式 rehash 可以避免一次卡顿(Redis 就是这么做的)。工程上还要考虑哈希函数的质量(防止碰撞攻击)、key 的可哈希性,以及迭代器失效规则。

Reactor 模式

Reactor 是事件驱动的 IO 处理模式:一个事件循环用 IO 多路复用(Linux 上是 epoll)等待事件,事件到达后分派给对应的处理器回调。组成是 Reactor(事件分发)、Acceptor(处理新连接)、Handler(读写与业务)。演进路线:单 Reactor 单线程(Redis 的网络部分思路)、单 Reactor 多线程(业务丢线程池)、主从 Reactor(主 Reactor 只管 accept,从 Reactor 管读写,Netty 和 muduo 都是这个结构)。要能对比 Proactor(异步 IO,由内核完成后回调,Windows IOCP)以及 epoll 的 LT 与 ET 区别(ET 必须一次把数据读干净,否则不再通知,通常配非阻塞 IO 循环读)。