14、继承与多态
继承表达“派生对象也是一种基类对象”,多态则让同一个基类接口在不同派生对象上表现不同。二者经常一起出现,但有继承关系,不代表调用一定发生动态分派。
先确定继承是否表达正确关系
本文使用 C++17。若只是复用一段实现,组合成员往往比继承更直接;public 继承通常应支持把派生对象当基类使用。不要因为两个类恰好有相似字段,就强行建立类型层次。
public、protected、private 继承影响基类成员在派生类中的可访问性与外部转换权限,不改变基类 private 成员不能被派生类直接访问的事实。派生类仍可通过基类公开或受保护的接口间接使用基类状态。
虚函数、覆盖与抽象类
通过基类指针或引用调用虚函数时,会根据对象的动态类型选择相应覆盖。使用 override 让编译器检查签名是否真的覆盖,避免把参数或 const 写错后误变成另一个函数。
带纯虚函数、尚未提供最终覆盖的类不能直接实例化,可以作为接口。若要通过基类指针删除派生对象,基类需要合适的虚析构函数。实现常用虚表,但 C++ 标准不要求某一种固定虚表布局。
C++17 多态示例
#include <cassert>
#include <iostream>
#include <memory>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Rectangle final : public Shape {
public:
Rectangle(double width, double height) : width_(width), height_(height) {}
double area() const override { return width_ * height_; }
private:
double width_;
double height_;
};
int main() {
std::unique_ptr<Shape> shape = std::make_unique<Rectangle>(3.0, 4.0);
assert(shape->area() == 12.0);
std::cout << shape->area() << '\n';
}
示例输出 12。智能指针负责所有权,虚函数负责行为分派,是两项不同机制。这里输入为正常非负边长,未把 Rectangle 扩展为完整的几何校验库。
多继承为什么出现两个基类子对象
设 B、C 都普通继承 A,D 同时继承 B 与 C。D 中会有沿两条路径得到的两个 A 子对象,d.x 因而可能有歧义;可以显式限定 B 或 C 路径,分别访问它们。

如果 B、C 改为虚继承 A,最派生的 D 中共享一个 A 虚基类子对象。这里的 virtual inheritance 与虚函数不是同一用途:前者控制共享基类子对象,后者控制虚函数调用行为。

虚继承不保证解决所有成员名冲突或最终覆盖歧义,也不能简单等同于“一定更省内存”;实现可能需要额外元数据。
C++17:直接验证子对象是否共享
#include <cassert>
#include <iostream>
struct A { int x = 0; };
struct B : A {};
struct C : A {};
struct D : B, C {};
struct VirtualB : virtual A {};
struct VirtualC : virtual A {};
struct SharedD : VirtualB, VirtualC {};
int main() {
D ordinary;
ordinary.B::x = 1;
ordinary.C::x = 2;
assert(&ordinary.B::x != &ordinary.C::x);
std::cout << ordinary.B::x << ' ' << ordinary.C::x << '\n';
SharedD shared;
shared.x = 3;
A& from_b = static_cast<VirtualB&>(shared);
A& from_c = static_cast<VirtualC&>(shared);
assert(&from_b == &from_c);
std::cout << from_b.x << ' ' << from_c.x << '\n';
}
输出 1 2 和 3 3。这验证的是逻辑子对象关系,不依赖它们在内存中的具体偏移。虚基类的初始化由最派生类负责,不应以某个中间类的构造列表推断最终初始化。
常见误区与练习
把派生对象按值复制到一个可实例化的基类对象中会发生切片,不能再期待派生部分保留下来。基类构造和析构期间的虚调用也不会像一个已完整构造的最派生对象那样向下分派;不要在此依赖派生状态。
解释两张菱形图中 A 子对象的数量,再说明为什么“加 virtual”并不是所有继承问题的通用修复。参考 C++17 派生类规则与 虚函数规则。
