继承使派生对象包含基类子对象。公开继承通常表达“派生对象可以作为基类对象使用”;组合则表达“一个对象拥有另一个对象”。运行时多态通过虚函数接口,让调用者在不依赖具体派生类型的情况下执行不同实现。
静态类型、动态类型与调用目标
跳转到“静态类型、动态类型与调用目标”在 Base* p = &derived; 中,表达式 p 的静态类型是 Base*,它指向的完整对象的动态类型是 Derived。成员查找、重载选择与访问检查首先依据静态类型;若最终调用的是虚函数,普通调用再选择该对象的最终覆盖函数。
| 调用形式 | 选择依据 | 例子 |
|---|---|---|
| 非虚成员调用 | 表达式的静态类型 | p->ordinary() 调用 Base 的版本 |
| 普通虚函数调用 | 对象的动态类型 | p->describe() 调用 Derived 的覆盖版本 |
| 显式限定的虚函数调用 | 指定的基类实现 | p->Base::describe() 不进行虚分派 |
| 重载调用 | 编译时的参数与候选函数 | print(1) 与 print(1.0) |
函数重载和模板常被称为静态多态,它们不要求继承。不能把“通过指针调用”直接等同于动态绑定;非虚成员经基类指针调用时仍使用基类实现。
一个完整、可运行的例子
跳转到“一个完整、可运行的例子”// C++17#include <cassert>#include <memory>#include <string>#include <vector>
std::vector<std::string> trace;
class Base {public: Base() { trace.push_back("Base constructed"); } virtual ~Base() { trace.push_back("Base destroyed"); } std::string ordinary() const { return "Base ordinary"; } virtual std::string describe() const { return "Base virtual"; }};
class Derived final : public Base {public: Derived() { trace.push_back("Derived constructed"); } ~Derived() override { trace.push_back("Derived destroyed"); } std::string ordinary() const { return "Derived ordinary"; } std::string describe() const override { return "Derived virtual"; }};
int main() { { std::unique_ptr<Base> object = std::make_unique<Derived>(); assert(object->ordinary() == "Base ordinary"); assert(object->describe() == "Derived virtual"); assert(object->Base::describe() == "Base virtual"); } assert((trace == std::vector<std::string>{ "Base constructed", "Derived constructed", "Derived destroyed", "Base destroyed" }));}派生类中的 ordinary() 隐藏同名基类函数,没有覆盖它,因为基类的该函数不是虚函数。派生 describe() 使用 override 验证签名;不必再次写 virtual,虚属性会延续。
覆盖、重载和隐藏要分开
跳转到“覆盖、重载和隐藏要分开”| 概念 | 要点 | 常见错误 |
|---|---|---|
| 覆盖 override | 覆盖基类虚函数,参数及相应限定需要匹配 | 把 f(int) 改成 f(double) 仍声称覆盖 |
| 重载 overload | 同名函数具有可区分的参数列表 | 仅改变返回类型来重载 |
| 名字隐藏 | 派生作用域同名声明可以遮住基类同名集合 | 调用派生对象时意外找不到基类重载 |
using Base::f | 把基类同名函数引入派生作用域参与查找 | 把 using 当作自动生成覆盖函数 |
覆盖函数返回类型一般必须相同,满足条件的类指针或类引用允许协变返回,例如基类返回 Base*、派生类返回 Derived*。不能任意改成另一种值类型。const、引用限定以及异常规格也要满足对应规则;例如基类声明 noexcept 时不能用可能抛异常的覆盖函数放宽约束。C++ 虚函数规则、异常规格
final 可禁止某个虚函数继续被覆盖,也可禁止类继续被继承。抽象类与纯虚函数见 抽象类、纯虚函数与析构。
构造和析构顺序
跳转到“构造和析构顺序”对于没有虚基类的普通单继承对象:先构造基类,再按成员声明顺序构造成员,最后执行派生构造函数体;析构时先执行派生析构函数体,再销毁成员,最后销毁基类。初始化列表书写顺序不能改变成员声明顺序。基类与成员初始化
不要在派生析构函数中手动调用 Base::~Base()。 基类析构会自动发生。手动调用后又自动调用会对寿命已经结束的对象再次析构,产生未定义行为,不是避免资源泄漏的方法。析构顺序及对象寿命
常规 delete Base* 如果实际对象是派生类,基类通常必须有虚析构函数,否则属于未定义行为,不能简单描述成“只少调用一次派生析构”。不允许经基类删除的接口,可采用受保护的非虚析构函数来阻止这种用法;无继承用途的普通值类型不必为此增加虚析构。delete 表达式
构造、析构期间的虚调用不会越过当前正在构造或析构的类去访问更派生部分。不要在基类构造时期待派生类覆盖函数已经可用。构造和析构期间的成员调用
vtable 是理解工具,不是固定布局保证
跳转到“vtable 是理解工具,不是固定布局保证”常见编译器用虚函数表与对象内的虚表指针实现动态分派。但 C++ 规定的是调用行为,并不要求虚指针一定在对象起始位置、一定只有一个、一定占 4 字节,也不要求虚调用每次都真的查表。多重继承、虚继承、ABI 和优化都会影响实现。
可以用“对象信息 → 对应虚函数入口 → 覆盖实现”帮助理解,不能据此直接读取固定偏移的指针或据此决定二进制文件布局。已知实际类型时,编译器可能去虚化并内联;性能结论应通过真实构建和测量确认。
struct、class 与对象切片
跳转到“struct、class 与对象切片”struct 和 class 都可以继承。区别在默认访问权限:struct 的成员及基类默认 public,class 的成员及基类默认 private;显式写出权限后遵循相同规则。原例 struct IoBinding : public Base<OrtIoBinding> 表示公开继承一个模板实例,并不是特殊的继承机制。
按值把 Derived 赋给 Base 会只保留基类部分,称为对象切片;此后 Base 对象不会凭空恢复派生行为。需要运行时多态时传引用或指针,并明确对象寿命。需要独立复制多态对象时,应设计专门的复制接口,而不是依赖基类按值复制。