Lazy loaded image
4. 面向对象与多态
Words 1769Read Time≈ 5 min
2026-9-4
🧭
核心理解
面向对象负责组织“数据与行为”,多态负责让同一个调用接口,根据类型选择不同实现。真正的设计目标不是继承层级越深越好,而是隔离变化、减少修改。

如何选择

4.1 封装、继承与组合

封装:守住对象边界

封装把数据和操作数据的方法放在一起,并通过访问控制避免外部随意破坏对象状态。

继承:表达“是一个”

当派生类可以替代基类,且不会破坏基类约定时,才适合继承。
Circle 是一个 Shape → 可以继承

组合:表达“有一个”

组合把对象作为成员使用,通过协作复用功能。
Car 有一个 Engine → 应该组合
判断问题
继承
组合
关系
“是一个”
“有一个 / 使用一个”
耦合程度
较高
较低
运行时替换
较难
可通过接口替换成员
主要价值
类型替换与动态多态
功能复用与灵活装配
✅
选择原则
优先组合;只有确实存在稳定的“是一个”关系,并且需要类型替换时,才使用公有继承。继承不是代码复用的万能胶,粘多了通常拆不下来。

4.2 静态多态

静态多态在编译期决定调用哪个实现,不需要虚函数分派。常见方式包括函数重载、运算符重载和模板。

函数重载

运算符重载

运算符重载应该保持原有语义。把 + 写成删除对象,语法上也许能过,设计上已经开始胡闹。

模板与编译期分派

优点是调用成本低、便于内联;代价是实现通常要放在头文件中,可能增加编译时间和生成代码体积。

4.3 动态多态

动态多态在运行期根据对象的实际类型选择实现。C++ 通常通过基类指针或引用、虚函数和函数重写实现。
notion image

虚函数

notion image

纯虚函数与抽象类

如果基类只定义统一接口,不提供合理的默认实现,可以声明纯虚函数:
包含纯虚函数的类称为抽象类,不能直接实例化;派生类必须实现纯虚函数,否则派生类仍是抽象类。
= 0 的意思不是“返回 0”,而是“该函数为纯虚函数”。

为什么基类析构函数要是虚函数

如果对象会通过基类指针释放,基类析构函数必须是虚函数:
否则通过基类指针删除派生类对象会产生未定义行为,派生类资源可能无法正确释放。

对象切片

把派生类对象按值赋给基类对象时,派生类独有部分会被截掉:
需要多态时,应使用基类引用或智能指针:

虚函数表如何完成分派

多数编译器使用虚函数表实现动态多态,但 C++ 标准并未强制具体实现。
典型过程:
  1. 带虚函数的对象内部保存一个虚表指针 vptr。
  1. vptr 指向该实际类型的虚函数表。
  1. 通过基类指针或引用调用虚函数时,从表中找到实际函数地址。
成本通常是对象多一个指针、调用多一次间接寻址,并可能限制内联。不要神化这点:多数业务代码真正昂贵的是糟糕的数据布局、内存分配和 I/O,不是一次虚函数调用。

4.4 两类多态对比

对比项
静态多态
动态多态
决定时间
编译期
运行期
常见机制
重载、运算符重载、模板
虚函数、继承、接口
调用成本
低,容易内联
通常有一次间接调用
类型要求
编译期类型已知
可在运行时替换对象
扩展方式
新增类型并重新编译
新增派生类,调用方可保持不变
主要代价
编译时间、代码膨胀
对象开销、间接调用、层级复杂度
适用场景
泛型算法、数值计算、性能敏感路径
插件、驱动抽象、策略替换、跨模块接口
🎯
最终判断
静态多态用编译期约束换性能,动态多态用少量运行期开销换可替换性。先判断“类型何时确定”,再选机制;不要为了展示会写 virtual,给三行代码造一棵家谱树。

小结

  • 封装保护对象状态,继承表达“是一个”,组合表达“使用一个”。
  • 静态多态在编译期分派,主要依靠重载和模板。
  • 动态多态在运行期分派,需要基类指针或引用与虚函数。
  • 抽象类定义共同接口;多态基类通常需要虚析构函数。
  • 按值保存派生类会发生对象切片,多态对象应使用引用或智能指针。
  • 优先组合,谨慎继承;继承层级越深,往往越难维护。
上一篇
C++ String 常用操作
下一篇
嵌入式工程师习题集

Comments
Loading...