第2章 · C++语言基础高频考点:内存管理、虚函数表、智能指针、move语义
本章聚焦面试中出现频率最高的四个C++语言特性。你将通过3个经典面试问答掌握回答框架,并借助智能指针回答框架图快速构建答题逻辑。适合用于45分钟限时突击复习。
- 内存管理 new/delete、RAII、内存泄漏检测
- 虚函数表 动态绑定、多态开销、虚函数地址布局
- 智能指针 引用计数、循环引用、性能取舍
- move语义 右值引用、完美转发、移动构造与赋值
Q1:请解释C++中的虚函数表(vtable)是如何工作的?
🎯 面试官考察点
- 是否理解动态多态的底层实现机制
- 对虚函数表指针、虚函数表布局、地址填充的掌握
- 能否说清虚函数调用时的性能代价
📐 回答框架
- 定义:每个包含虚函数的类都有一个虚函数表(vtable),它是一个函数指针数组。
- 对象布局:对象首部存放一个虚表指针(vptr),指向该类的虚函数表。
- 多态调用:通过 vptr 找到 vtable,再通过索引找到实际函数地址,实现动态绑定。
- 继承场景:派生类会重写虚函数地址,多重继承时可能有多个 vptr。
- 开销:内存上增加一个指针,调用上多一次间接跳转,且无法内联。
💡 简短示例回答
每个含有虚函数的类在编译期生成一张虚函数表,里面按声明顺序存放虚函数地址。每个对象创建时被赋予一个 vptr 指向该类虚表。调用虚函数时,先通过 vptr 找到虚表,再根据函数在虚表中的偏移找到实际地址。例如 Base *pb = new Derived; pb->foo(); 会调用 Derived::foo(),这就是动态绑定。
🔍 可能追问
- 虚函数表存放在哪个段(只读数据段 .rodata)?
- 构造函数中调用虚函数能体现多态吗?(不能,因为派生类 vptr 尚未赋值)
- 纯虚函数的 vtable 条目是什么?(指向一个 abort 函数或 nullptr)
⚠️ 避坑提醒
不要混淆 vtable 和 RTTI(运行时常量识别)。vtable 只负责函数地址查找,type_info 是另一张表。回答时不要遗漏“虚表在编译期确定,vptr 在构造时赋值”这一关键点。
Q2:请比较 shared_ptr 和 unique_ptr 的区别,并说明何时使用 weak_ptr?
🎯 面试官考察点
- 是否了解智能指针的不同所有权模型
- 对引用计数实现与控制块的理解
- 能否解释循环引用及 weak_ptr 的破局方法
📐 回答框架
- unique_ptr:独占所有权,不可拷贝只能移动,零开销抽象,适合作用域内资源。
- shared_ptr:共享所有权,引用计数,拷贝使计数+1,析构使计数-1,计数为0时释放资源。
- weak_ptr:不增加引用计数,用于观察 shared_ptr,通过 lock() 获取临时 shared_ptr。
- 典型应用:unique_ptr 用于工厂返回、容器存储;shared_ptr 用于多线程共享资源;weak_ptr 用于打破循环引用。
💡 简短示例回答
unique_ptr 独占资源,移动语义转移所有权。shared_ptr 使用引用计数控制块,拷贝时计数+1。当两个 shared_ptr 互相引用时形成循环,可用 weak_ptr 打破:class A { shared_ptr<B> b; } + class B { weak_ptr<A> a; }。weak_ptr 不参与计数,资源可正常释放。
🔍 可能追问
- shared_ptr 的控制块和资源块是分开的吗?(是,控制块在堆上,资源可以自定义删除器)
- 如何将 unique_ptr 转换为 shared_ptr?(直接移动构造,共享所有权)
- weak_ptr::lock() 的线程安全如何保障?(控制块用原子操作,lock 内部增加计数时判断)
⚠️ 避坑提醒
不要回答“shared_ptr 比 unique_ptr 更强大”,面试官期望你理解 unique_ptr 的零开销优势。避免使用 make_shared 而手动 new shared_ptr,可能导致控制块和资源块分离,性能下降。
Q3:解释 C++11 move 语义及其实现原理。如何避免使用 move 后对象处于“有效但未指定”状态?
🎯 面试官考察点
- 对右值引用和移动构造/赋值函数的理解
- 能否讲清 std::move 仅仅是类型转换,不搬数据
- 是否了解移动后的对象必须保证析构安全
📐 回答框架
- 移动语义目的:避免临时对象的深拷贝,转移资源所有权。
- 实现手段:定义移动构造函数和移动赋值函数,参数为右值引用(T&&)。
- std::move:只是一个 static_cast 到右值引用,触发移动而非拷贝。
- 后状态要求:移动后的对象必须为有效且可析构(通常置为 null 或空状态),不能依赖其值。
- 最佳实践:std::vector 等标准库在 reallocation 时优先使用移动,若没有移动则拷贝。
💡 简短示例回答
移动构造函数将源对象资源指针“偷”过来,然后将源对象置为空。例如 MyString(MyString&& other) noexcept : data(other.data) { other.data = nullptr; }。std::move(&x) 不产生任何代码,只是让编译器选择移动版本。移动后的 x 必须有效可析构,但不应再读取其内容。
🔍 可能追问
- 移动构造函数为什么不抛出异常?(noexcept 保证容器使用强异常安全时优先选移动)
- 自移动赋值是否安全?(需要自检,避免释放自己)
- 完美转发 std::forward 与 std::move 的区别?(forward 保持左/右值属性,move 无条件转右值)
⚠️ 避坑提醒
不要回答“移动后的对象不能再用”,标准要求的是“有效但未指定状态”(valid but unspecified)。例如 std::string 被 move 后可能为空,但调用 size() 是合法的。也不能假设移动后对象资源被清空,只能依赖标准保证可析构。
📌 本章核心速查表(内存管理 + 智能指针 + 虚表 + move)
| 考点 | 一句话要点 | 易错点 |
|---|---|---|
| new/delete | new 分配 + 调用构造,delete 调用析构 + 释放;必须匹配 new[] / delete[] | 误用 free() 释放 new 对象或忘记数组括号 |
| 虚函数表 | 每个类一张表,对象首部 vptr;多态通过 vtable 索引实现 | 构造函数内虚函数不体现多态;多重继承 vptr 数量 |
| smart_ptr | unique_ptr 独占且零开销;shared_ptr 引用计数控制块;weak_ptr 不增计数 | 循环引用未用 weak_ptr;误用 auto_ptr(已被弃用) |
| move语义 | 右值引用 + 移动构造转移资源;std::move 仅转换类型 | 忘记 noexcept;移动后对象仍被使用;自移动未处理 |
💡 面试提分建议
本章四个考点常常混合考察,例如“请设计一个不可拷贝但可移动的智能指针”“为什么优先使用 make_shared 而不是手动 new”。建议在组内模拟面试时,将虚表与移动语义结合思考(如移动后 vptr 是否变化?答案:vptr 不变,因为类型已定)。