第六章 继承、多态、类型转换与异常
第六章 继承、多态、类型转换与异常
1. 重载、重写和隐藏有什么区别?
问题分析
这道题不只是让你罗列名词,而是考查能否从继承关系、动态类型与异常边界做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:重载发生在同一作用域,同名函数拥有不同参数列表,由编译器做重载决议。
- 再讲机制:围绕“三个概念”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯游戏客户端一面
问题讲解
一、三个概念
重载发生在同一作用域,同名函数拥有不同参数列表,由编译器做重载决议。重写是派生类对基类虚函数提供同签名覆盖,运行期根据动态类型派发。隐藏是派生作用域声明同名成员后,基类同名集合在普通名字查找中被遮蔽,即使参数不同也可能发生。
| 概念 | 发生位置 | 是否要求 virtual | 选择时机 |
|---|---|---|---|
| 重载 | 同一作用域 | 否 | 编译期 |
| 重写 | 基类与派生类 | 基类是虚函数 | 运行期派发 |
| 隐藏 | 基类与派生类同名 | 否 | 名字查找阶段 |
派生类可写 using Base::f; 把基类重载集合引入当前作用域。重写应加 override,签名不一致时编译器立即报错。
二、完整可执行示例
示例同时展示重载集合、using 消除隐藏,以及 override 驱动的动态派发。若把派生函数的 const 删除,override 会让签名错误在编译期暴露。
#include <iostream>
struct Base {
void print(int value) const { std::cout << "Base int " << value << '\n'; }
virtual void print(double value) const {
std::cout << "Base double " << value << '\n';
}
virtual ~Base() = default;
};
struct Derived : Base {
using Base::print;
void print(double value) const override {
std::cout << "Derived double " << value << '\n';
}
};
int main() {
Derived derived;
derived.print(7); // using 保留 Base::print(int)
const Base& base = derived;
base.print(3.5); // 虚派发到 Derived
}

三、常见错误
- 把派生类同名非虚函数称为重写。
- 忘记 const/ref 限定,实际未覆盖。
- 认为返回类型任意不同也能重写;只允许有限协变返回。
- 用强转解决隐藏,而不是
using或重新设计接口。
回答自检
- 三者的作用域和选择时机。
- 名字查找、重载决议和虚派发的顺序。
using、override、final的用途。- 默认参数与虚函数的边界。
面试官可能追问的问题
override是否影响运行效率? 不影响语义成本,主要提供编译期校验。final有什么作用? 禁止继续覆盖虚函数或继承类。- 默认参数参与虚派发吗? 默认实参按静态类型在编译期确定。
2. 异常和错误码如何选择?
问题分析
这道题不只是让你罗列名词,而是考查能否从继承关系、动态类型与异常边界做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:异常向匹配的
catch传播并展开栈时,离开作用域的已构造局部对象按逆序析构;未捕获异常或触发std::terminate时不能一概保证完成全部展开,这正是 RAII 能覆盖异常路径的原因。 - 再讲机制:围绕“异常处理的传播路径 → 两种机制 → 工程选择”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯后台开发二面
问题讲解
补充:异常处理的传播路径
异常向匹配的 catch 传播并展开栈时,离开作用域的已构造局部对象按逆序析构;未捕获异常或触发 std::terminate 时不能一概保证完成全部展开,这正是 RAII 能覆盖异常路径的原因。异常类型应表达可处理的失败类别;跨 C ABI、析构函数、实时线程或明确禁用异常的模块,通常在边界处转换为错误码。

一、两种机制
错误码把失败放在返回值或输出参数中,控制流显式、适合预期内高频失败和禁用异常的系统,但调用者可能遗漏检查。异常把正常返回与失败传播分开,可跨多层调用并配合 RAII 自动清理,适合无法就地处理的异常情况;其抛出路径成本较高且影响控制流分析。
二、工程选择
查找不到、非阻塞暂不可读等常属于正常状态,不宜抛异常。构造对象无法建立不变式、深层操作无法恢复时,异常可减少逐层转发样板。跨 C ABI、插件、实时线程或内核边界通常转为错误码。C++23 std::expected 提供类型化返回值;C++17 可用项目内 Result 类型。
三、常见错误
- 用异常处理普通分支或循环终止。
- 捕获后静默吞掉,既不恢复也不记录。
- 返回错误码却不强制检查。
- 让异常穿过不兼容 ABI 或析构函数。
回答自检
- 两种机制的控制流和检查方式。
- 预期失败与异常失败的区别。
- RAII 对异常传播的重要性。
- ABI、实时性和团队规范如何影响选择。
面试官可能追问的问题
- 异常是否总是很慢? 未抛出路径在常见实现中成本较低,真正抛出和展开通常昂贵。
- 错误码如何携带上下文? 使用类型化错误、消息、因果链,而非只返回整数。
- 边界如何转换? 捕获内部异常,记录上下文,再映射为稳定错误协议。
3. 什么是运行时多态?虚函数调用是如何实现的?
问题分析
这道题考查能否把继承关系、动态类型与异常边界转化为可执行的判断步骤,而不是只报出 API 名称或最终结论。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:通过基类指针或引用调用虚函数时,根据对象的动态类型选择最终覆盖函数,这就是运行时多态。
- 再讲机制:围绕“语言语义 → 常见实现与边界”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:美团 C++ 后端二面
问题讲解
一、语言语义
通过基类指针或引用调用虚函数时,根据对象的动态类型选择最终覆盖函数,这就是运行时多态。前提是对象没有被切片、调用目标是虚函数且访问路径保留多态对象身份。
二、常见实现与边界
主流 ABI 通常让多态对象含虚表指针,虚表槽保存函数入口,调用需要一次或多次间接寻址;多继承可能还需调整 this。但 C++ 标准只规定可观察行为,不规定必须存在 vptr/vtable、它们的位置或数量。优化器知道动态类型时可去虚拟化并内联。
图示主流ABI的常见实现,标准不规定虚表布局。
三、常见错误
- 把虚表布局和对象头大小当作标准保证。
- 认为任何基类同名函数都会动态派发。
- 按值传递后仍期待派生实现。
- 为所有函数加 virtual,却没有稳定抽象边界。
回答自检
- 静态类型、动态类型和最终覆盖。
- 基类指针或引用是常见观察入口,不是虚派发的唯一语法;显式限定调用会抑制虚派发。
- 常见虚表调用路径。
- 标准保证与 ABI 实现的分界。
- 去虚拟化的可能性。
面试官可能追问的问题
- 虚函数成本多大? 通常一次间接调用,真正影响还包括内联受限和数据局部性。
- 模板多态有何不同? 编译期为具体类型生成代码,利于内联但可能增加代码体积。
- 虚表放在哪里? 属于实现/ABI 细节,常在只读区域。
4. 为什么多态基类的析构函数通常应该是虚函数?
问题分析
这道题考查能否从可观察现象追溯到继承关系、动态类型与异常边界中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:若对象实际类型是 Derived,却通过
Base*执行delete,基类析构非虚时行为未定义。 - 再讲机制:围绕“问题场景 → 设计规则”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:百度 C++ 一面
问题讲解
一、问题场景
若对象实际类型是 Derived,却通过 Base* 执行 delete,基类析构非虚时行为未定义。虚析构让删除表达式根据动态类型先执行派生析构,再执行基类析构,并使用匹配的释放逻辑。

二、设计规则
将作为多态接口并允许经基类所有权销毁的基类,应有 public virtual 析构。若明确禁止通过基类删除,可使用 protected 非虚析构,并由其他机制管理生命周期。虚析构不是所有类都需要,它会引入多态对象模型和 ABI 约束。
三、常见错误
- 认为只要类有任意虚函数,析构就自动虚。
- 只在派生类把析构写 virtual,却忽略基类接口。
- 认为没有派生资源时非虚删除也安全。
- 用
shared_ptr<Base>就认为不需要分析析构;构造方式会影响删除器,但接口设计仍应清楚。
回答自检
- 基类指针删除派生对象的调用链。
- 非虚删除为何未定义。
- public virtual 与 protected nonvirtual 两种设计。
- 智能指针不能替代析构契约。
面试官可能追问的问题
- 纯虚析构可以吗? 可以,但仍必须提供定义,因为派生析构后会调用它。
- 虚析构会让对象变大吗? 若类原本无虚函数,常见实现会增加虚表指针,但不作标准大小承诺。
unique_ptr<Base>有何要求? 默认删除器通过 Base* 删除,因此基类通常需虚析构。
5. 纯虚函数、抽象类和接口类有什么关系?
问题分析
这道题不只是让你罗列名词,而是考查能否从继承关系、动态类型与异常边界做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:纯虚函数用
= 0声明。 - 再讲机制:围绕“定义关系”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯客户端一面
问题讲解
一、定义关系
纯虚函数用 = 0 声明。只要类仍有至少一个未被具体覆盖的纯虚函数,它就是抽象类,不能直接实例化,但可以拥有数据成员、普通函数和构造函数。接口类是工程约定,通常只有纯虚操作和虚析构,不保存业务状态;C++ 没有专门的 interface 关键字。
纯虚函数可以有定义,派生类仍需覆盖后才能变为具体类;基类可通过限定名调用该定义。抽象基类的构造函数用于初始化基类子对象,并非因为不能直接实例化就不需要构造。
二、常见错误
- 认为抽象类只能有纯虚函数。
- 忘记接口基类的虚析构。
- 在接口中不断添加可变状态,破坏职责单一。
- 认为纯虚函数绝对不能有函数体。
回答自检
- 三个概念之间的包含关系。
- 抽象类可以拥有哪些成员。
- 纯虚函数可有定义的边界。
- 接口类作为工程约定的价值。
面试官可能追问的问题
- 纯虚析构是否要定义? 要,派生对象销毁会执行基类析构。
- 抽象类能否有对象成员? 能,只是不能直接创建该抽象类对象。
- 接口为何常无状态? 降低耦合,避免多继承中的状态和布局问题。
6. 构造函数为什么不能是虚函数?析构函数为什么可以?
问题分析
这道题考查能否从可观察现象追溯到继承关系、动态类型与异常边界中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:构造调用前必须已经知道要创建的完整类型及所需大小,且对象的派生部分尚未建立,无法依靠一个尚不存在的动态对象选择构造函数。
- 再讲机制:围绕“生命周期方向不同”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:字节客户端一面
问题讲解
一、生命周期方向不同
构造调用前必须已经知道要创建的完整类型及所需大小,且对象的派生部分尚未建立,无法依靠一个尚不存在的动态对象选择构造函数。不能把选择待创建类型交给一个尚未建立的动态对象;这是与虚析构不同的生命周期方向。C++11 起可用 using Base::Base 继承构造声明,但它不构成虚构造。参见继承构造函数。析构发生在完整对象已存在之后,可以通过动态类型选择最派生析构,再逐级销毁。
需要“虚构造”效果时使用工厂函数:根据配置选择具体类型并返回 unique_ptr<Base>。复制多态对象常用虚 clone()。
二、常见错误
- 说构造函数不能虚只是因为“语法规定”,没有生命周期解释。
- 在基类构造中期待虚调用进入派生实现。
- 用返回裸指针的工厂泄露所有权。
- 把 clone 返回 Base 值,造成切片。
回答自检
- 创建前为何必须知道具体类型。
- 完整动态类型何时建立。
- 虚析构的逆向销毁链。
- 工厂和 clone 如何提供“虚构造”效果。
面试官可能追问的问题
- 构造函数能否被继承? 可用
using Base::Base继承构造声明,但不是虚派发。 - 工厂如何返回多态对象? 通常返回
unique_ptr<Base>。 - 析构的虚派发何时停止? 每一级析构期间动态行为收缩到当前正在析构的类。
7. 在构造函数和析构函数中调用虚函数会发生什么?
问题分析
这道题考查能否用继承关系、动态类型与异常边界解释题目中的语义,而不是停留在定义记忆。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:在基类构造函数执行时,派生部分尚未构造;
- 再讲机制:围绕“派发规则 → 替代设计”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:美团 C++ 二面
问题讲解
一、派发规则
在基类构造函数执行时,派生部分尚未构造;在基类析构函数执行时,派生部分已经销毁。因此从构造/析构函数调用虚函数,只会派发到当前正在构造或析构的类层级,不会调用更派生覆盖。
这防止访问尚未初始化或已经销毁的派生成员。从构造或析构路径对该对象的纯虚函数进行虚调用是未定义行为,即使纯虚函数另外提供了定义也不能据此使虚调用合法。显式限定的非虚调用(如 Base::f())在有定义时是另一种情况。参见抽象类与纯虚调用。
二、替代设计
不要依赖构造中的虚钩子完成派生初始化。可用工厂两阶段组装、非虚接口加构造后显式启动,或把所需策略作为构造参数传入。析构阶段只做当前层级资源清理。
三、常见错误
- 认为 virtual 在任何位置都按最派生类型派发。
- 基类构造调用派生逻辑维护不变式。
- 析构中调用需要派生资源的虚函数。
- 用日志观察某 ABI 行为后当成对象始终完整。
回答自检
- 构造与析构期间动态行为为何收缩。
- 防止访问未生/已灭派生成员的原因。
- 纯虚调用风险。
- 工厂或策略注入的替代设计。
面试官可能追问的问题
- 构造函数体内对象的动态类型是什么? 虚派发视为当前构造层级。
- 能否调用有定义的纯虚函数? 通过限定名可调用其定义,但普通虚调用边界仍需谨慎。
- 如何确保构造后执行派生初始化? 用工厂在完整构造后调用明确的启动步骤。
8. 四种 C++ 类型转换分别适合什么场景?
问题分析
这道题不只是让你罗列名词,而是考查能否从继承关系、动态类型与异常边界做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:
static_cast用于编译期可验证的数值转换、明确的继承上行及用户定义转换; - 再讲机制:围绕“四种转换”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:百度 C++ 后端一面
问题讲解
一、四种转换
static_cast 用于编译期可验证的数值转换、明确的继承上行及用户定义转换;基类到派生类的下行若对象实际类型不匹配会出错。dynamic_cast 在多态层次进行运行期检查,指针失败返回空,引用失败抛 bad_cast。const_cast 只调整 cv 限定。reinterpret_cast 进行低层表示重解释,最危险且不创造合法对象。
| 转换 | 主要用途 | 失败/风险 |
|---|---|---|
static_cast | 数值、明确类型关系 | 窄化、错误下行不可检测 |
dynamic_cast | 多态安全下行/横向 | 需 RTTI 和多态基类 |
const_cast | 调整 const/volatile | 写原生 const 对象是 UB |
reinterpret_cast | 系统级表示转换 | 对齐、别名、生命周期风险 |
二、常见错误
- 用 C 风格转换掩盖实际执行了哪类危险操作。
static_cast<Derived*>后不确认动态类型。const_cast后修改原本声明为 const 的对象。- 通过 reinterpret_cast 违反对齐、严格别名或对象生命周期。
回答自检
- 四种转换的语义边界。
- 安全上行与需检查下行。
- 移除 const 后写入的条件。
- 为什么 C 风格转换不利于审查。
面试官可能追问的问题
- 上行转换需要 dynamic_cast 吗? 通常不需要,public 继承上行是安全隐式转换。
- dynamic_cast 需要什么? 涉及向下/横向时源基类通常必须是多态类型。
- 序列化能否直接 reinterpret_cast 整个对象? 通常不安全,受填充、端序、ABI 和生命周期影响。
9. 空类、普通类和含虚函数类的对象大小如何分析?
问题分析
这道题考查能否把继承关系、动态类型与异常边界转化为可执行的判断步骤,而不是只报出 API 名称或最终结论。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:完整空类对象必须具有非零大小,使同类型不同对象可有不同地址,常见实现为 1 字节。
- 再讲机制:围绕“分析因素 → 边界”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯客户端二面
问题讲解
一、分析因素
完整空类对象必须具有非零大小,使同类型不同对象可有不同地址,常见实现为 1 字节。普通类大小受非静态数据成员、对齐和尾部填充影响,静态成员不在每个对象内。含虚函数的类在主流 ABI 中通常含虚表指针,但标准不规定其存在、位置或数量。
二、边界
空基类优化允许空基类子对象不额外占空间;C++20 [[no_unique_address]] 可允许空成员复用地址。多继承、虚继承会改变布局。最终结论应在目标编译器、ABI 和编译选项下用 sizeof/alignof 验证,不能背固定数字。
三、常见错误
- 把成员大小简单相加,忽略排列和尾部填充。
- 把静态成员计入对象。
- 断言所有多态类只有一个 vptr。
- 用
sizeof推断可序列化的有效字段布局。
回答自检
- 非静态成员、基类、对齐的贡献。
- 静态成员为何不计入对象。
- 空对象的地址要求和空基类优化。
- 虚表指针为何是常见实现而非标准保证。
面试官可能追问的问题
- 成员顺序会影响大小吗? 会,按对齐重新排列成员可减少填充,但会影响 ABI。
- 空类为什么非零? 确保同类型不同完整对象有可区分地址。
- 对象能否直接 memcpy 到网络? 通常不能,填充、端序、指针和 ABI 都不稳定。
10. RTTI、dynamic_cast 和 typeid 分别解决什么问题?
问题分析
这道题考查能否用继承关系、动态类型与异常边界解释题目中的语义,而不是停留在定义记忆。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:RTTI 是运行时类型信息机制,支持
dynamic_cast和typeid。 - 再讲机制:围绕“三者关系 → 选择与设计”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合继承关系、动态类型与异常边界说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:小鹏 C++ 二面
问题讲解
一、三者关系
RTTI 是运行时类型信息机制,支持 dynamic_cast 和 typeid。dynamic_cast 用于多态继承层次中的安全下行或横向转换;typeid(expr) 返回 type_info,对多态左值表达式可反映动态类型,对非多态表达式通常反映静态类型。

二、选择与设计
偶尔从通用接口获取特定扩展能力时,dynamic_cast 合理。若代码到处判断具体派生类型,往往说明基类缺少所需虚操作,或应使用 visitor/variant。type_info::name() 的字符串不保证可读、稳定或跨编译器一致,不能作为持久化协议。
三、常见错误
- 对非多态基类做需要运行期检查的下行转换。
- 使用
typeid(x).name()做稳定业务标识。 - 用 dynamic_cast 链代替可扩展的虚接口。
- 认为关闭 RTTI 后语言所有虚函数都不能用。
回答自检
- RTTI 为哪两个设施提供支持。
- dynamic_cast 指针和引用失败行为。
- typeid 何时观察动态类型。
- 大量类型判断为何可能是设计信号。
- 类型名字符串为何不能当稳定协议。
面试官可能追问的问题
typeid(*p)中 p 为空会怎样? 若表达式是多态间接访问,会抛std::bad_typeid。- dynamic_cast 性能如何? 与继承结构和 ABI 实现有关,不应放在极热路径而不测量。
- RTTI 与虚表关系是标准规定吗? 不是,具体元数据布局属于实现。




