面灵AI→

华为后端开发岗面经:C++ 移动语义与 STL 容器

时间
2026-09
来源
牛客网

《面试题目》

面经 01(面试时间未知)

  1. C++ 移动语义有什么作用?
  2. unique_ptr、shared_ptr 和 weak_ptr 各有什么特点和作用?
  3. C++ 编译期多态和运行期多态有什么区别?
  4. 虚指针、虚函数和虚函数表如何实现运行期多态?
  5. vector、stack 和 priority_queue 等 STL 容器分别有什么特点?
  6. Linux 进程间通信有哪些机制?
  7. 单调栈、滑动窗口和哈希优化分别适用于哪些算法场景?

原帖为连载合集,面经 02 的正文在牛客服务端即被截断,此处只保留已发布的部分。

《参考解析》

移动语义与右值引用

移动语义的目的是避免不必要的深拷贝。右值引用 T&& 可以绑定临时对象,移动构造 / 移动赋值做的事情是「窃取」源对象的资源——把内部指针直接搬过来,再把源的指针置空,复杂度从 O(n) 降到 O(1)。要澄清一个高频误区:std::move 本身不移动任何东西,它只是把左值转换成右值引用,真正的移动发生在重载决议选中移动构造 / 移动赋值之后;用完的对象处于「有效但未指定」状态,只能析构或重新赋值。工程要点:定义了析构、拷贝构造、拷贝赋值中任意一个,就该考虑五法则把移动版本补上;移动构造要标 noexcept,否则 std::vector 扩容时为满足强异常安全保证会退回拷贝;泛型转发用 std::forward<T> 保留值类别,别用 std::move。

三种智能指针的差异与坑

unique_ptr 独占所有权,不可拷贝只可移动,默认删除器下大小与裸指针一致、零额外开销,用 std::make_unique 创建,适合所有权明确的资源。shared_ptr 共享所有权,控制块里存强引用计数和弱引用计数,计数用原子操作因此有开销(典型大小是对象指针 + 控制块指针共 16 字节),循环引用会导致泄漏。weak_ptr 不增加强计数,通过 lock() 尝试提升为 shared_ptr(返回空说明对象已析构),专门用来打破循环引用(父子互指、观察者缓存、缓存里持有 key 的场景)。坑有四个:同一个裸指针构造两次 shared_ptr 会产生两个独立控制块,最终 double free;shared_ptr 的引用计数操作对同一对象是线程安全的,但对同一个 shared_ptr 实例的并发读写不是;成员函数里需要自身 shared_ptr 时要继承 enable_shared_from_this,不能直接 shared_ptr<T>(this);unique_ptr 的删除器是类型的一部分(影响类型),shared_ptr 的删除器不是(只影响控制块)。

编译期多态与运行期多态,虚函数表怎么工作

编译期多态靠模板实例化和函数重载,调用在编译期就定下来,零运行时开销,代价是代码膨胀、报错信息难读。运行期多态靠继承加虚函数,通过基类指针或引用调用时动态绑定。实现机制是:含虚函数的类,每个对象头部有一个 vptr,指向该类共享的虚函数表 vtable;vtable 在编译期生成,按声明顺序存放虚函数地址,派生类重写某个虚函数时就把对应表项替换成派生类的函数地址。多重继承下对象会有多个 vptr 和对应的多张表,通过第二基类指针调用时需要调整 this 偏移(编译器生成 thunk)。三个常被追问的点:构造函数和析构函数里调用虚函数不会走动态绑定(此时 vptr 指向当前正在构造的类);基类析构函数必须声明为虚函数,否则通过基类指针 delete 派生对象是未定义行为,只会调用基类析构,派生类的资源泄漏;final 和 override 关键字分别用于阻止重写和让编译器帮你检查重写签名。

vector、stack、priority_queue 的特点

vector 是连续内存的动态数组,随机访问 O(1)、尾部 push_back 平摊 O(1)、中间插入删除 O(n);扩容按 1.5~2 倍增长并把元素搬过去,因此扩容后所有迭代器、指针和引用全部失效——不要在范围 for 里 push_back,需要稳定地址时用 deque 或 list,或者提前 reserve。stack 是容器适配器(默认底层 deque),LIFO,只暴露 top/push/pop,没有迭代器因而不能遍历。priority_queue 也是适配器(默认底层 vector 加 std::make_heap 维护大顶堆),top() O(1)、push/pop O(log n),同样没有迭代器,要小顶堆得写 priority_queue<T, vector<T>, greater<T>>;它不支持修改堆中元素的优先级,需要 decrease-key 的场景要自己实现可索引堆或用惰性删除(重复入堆、弹出时校验版本)。另外两个冷门点:deque 是分段连续,两端插入 O(1) 但中间插入 O(n);vector<bool> 是特化实现,按位存储,取不到元素的真实地址。

Linux 进程间通信有哪些机制

管道 pipe:匿名、半双工、只能用于有亲缘关系的进程;命名管道 FIFO 在文件系统里有名字,无亲缘关系也能用。消息队列:内核维护、有消息边界,System V 和 POSIX 两套 API,适合传结构化消息但不适合大数据量。共享内存:最快,一次映射后零拷贝,但必须自己配信号量或互斥锁做同步,否则就是数据竞争;POSIX 用 shm_open + mmap,用完要 shm_unlink,否则进程崩溃后会残留。信号量与信号:前者用于计数同步不传数据,后者是异步通知、信息量小(实时信号可以带一点数据),信号处理函数里只能调用异步信号安全函数。Socket:唯一能跨主机的方案,本机通信可以用 AF_UNIX 域套接字,比走 TCP 回环还快。选型口诀:大数据低延迟用共享内存加信号量,父子简单通信用管道,跨机用 socket,事件通知用信号或 eventfd。

单调栈、滑动窗口与哈希优化的适用场景

单调栈适合「找每个元素左边或右边第一个比它大 / 小的元素」这一类问题:下一个更大元素、每日温度、柱状图中最大矩形、接雨水,做法是维护一个单调的栈,每个元素最多进栈出栈一次,整体 O(n)。滑动窗口适合「连续子数组或子串满足某个单调条件」:最长无重复字符子串、最小覆盖子串、和大于等于 target 的最短子数组;使用前提是窗口扩大和缩小时判定条件单调(都是正数时才成立),数组含负数时求和类问题不满足单调性,要换成前缀和加哈希。哈希优化适合「判断存在性、计数、找补数」:两数之和、和为 k 的子数组、最长连续序列,代价是 O(n) 额外空间;C++ 里常用 unordered_map(平均 O(1)、最坏 O(n)),配合「前缀和 + 哈希表只记录首次出现的下标」是极高频的组合。实战里这三者经常混用:先想清楚要维护的是「一个单调序列」「一个区间」还是「一张计数表」,再选结构。