核心理解
面向对象负责组织“数据与行为”,多态负责让同一个调用接口,根据类型选择不同实现。真正的设计目标不是继承层级越深越好,而是隔离变化、减少修改。
如何选择
4.1 封装、继承与组合
封装:守住对象边界
封装把数据和操作数据的方法放在一起,并通过访问控制避免外部随意破坏对象状态。
继承:表达“是一个”
当派生类可以替代基类,且不会破坏基类约定时,才适合继承。
Circle 是一个 Shape → 可以继承组合:表达“有一个”
组合把对象作为成员使用,通过协作复用功能。
Car 有一个 Engine → 应该组合判断问题 | 继承 | 组合 |
关系 | “是一个” | “有一个 / 使用一个” |
耦合程度 | 较高 | 较低 |
运行时替换 | 较难 | 可通过接口替换成员 |
主要价值 | 类型替换与动态多态 | 功能复用与灵活装配 |
选择原则
优先组合;只有确实存在稳定的“是一个”关系,并且需要类型替换时,才使用公有继承。继承不是代码复用的万能胶,粘多了通常拆不下来。
4.2 静态多态
静态多态在编译期决定调用哪个实现,不需要虚函数分派。常见方式包括函数重载、运算符重载和模板。
函数重载
运算符重载
运算符重载应该保持原有语义。把
+ 写成删除对象,语法上也许能过,设计上已经开始胡闹。模板与编译期分派
优点是调用成本低、便于内联;代价是实现通常要放在头文件中,可能增加编译时间和生成代码体积。
4.3 动态多态
动态多态在运行期根据对象的实际类型选择实现。C++ 通常通过基类指针或引用、虚函数和函数重写实现。

虚函数

纯虚函数与抽象类
如果基类只定义统一接口,不提供合理的默认实现,可以声明纯虚函数:
包含纯虚函数的类称为抽象类,不能直接实例化;派生类必须实现纯虚函数,否则派生类仍是抽象类。
= 0 的意思不是“返回 0”,而是“该函数为纯虚函数”。为什么基类析构函数要是虚函数
如果对象会通过基类指针释放,基类析构函数必须是虚函数:
否则通过基类指针删除派生类对象会产生未定义行为,派生类资源可能无法正确释放。
对象切片
把派生类对象按值赋给基类对象时,派生类独有部分会被截掉:
需要多态时,应使用基类引用或智能指针:
虚函数表如何完成分派
多数编译器使用虚函数表实现动态多态,但 C++ 标准并未强制具体实现。
典型过程:
- 带虚函数的对象内部保存一个虚表指针
vptr。
vptr指向该实际类型的虚函数表。
- 通过基类指针或引用调用虚函数时,从表中找到实际函数地址。
成本通常是对象多一个指针、调用多一次间接寻址,并可能限制内联。不要神化这点:多数业务代码真正昂贵的是糟糕的数据布局、内存分配和 I/O,不是一次虚函数调用。
4.4 两类多态对比
对比项 | 静态多态 | 动态多态 |
决定时间 | 编译期 | 运行期 |
常见机制 | 重载、运算符重载、模板 | 虚函数、继承、接口 |
调用成本 | 低,容易内联 | 通常有一次间接调用 |
类型要求 | 编译期类型已知 | 可在运行时替换对象 |
扩展方式 | 新增类型并重新编译 | 新增派生类,调用方可保持不变 |
主要代价 | 编译时间、代码膨胀 | 对象开销、间接调用、层级复杂度 |
适用场景 | 泛型算法、数值计算、性能敏感路径 | 插件、驱动抽象、策略替换、跨模块接口 |
最终判断
静态多态用编译期约束换性能,动态多态用少量运行期开销换可替换性。先判断“类型何时确定”,再选机制;不要为了展示会写
virtual,给三行代码造一棵家谱树。小结
- 封装保护对象状态,继承表达“是一个”,组合表达“使用一个”。
- 静态多态在编译期分派,主要依靠重载和模板。
- 动态多态在运行期分派,需要基类指针或引用与虚函数。
- 抽象类定义共同接口;多态基类通常需要虚析构函数。
- 按值保存派生类会发生对象切片,多态对象应使用引用或智能指针。
- 优先组合,谨慎继承;继承层级越深,往往越难维护。

