1.4. 多重继承

C++ 中一个类可以从多个基类派生。回到鸟类的例子,可以做一个观察 —— 虽然会飞的鸟类彼此之间有很多共同点,但它们也与其他会飞的动物有共同之处,特别是飞行的能力。由于飞行并不局限于鸟类,我们可能希望,将与飞行处理相关的数据和算法移到一个独立的基类中。但不可否认的是,鹰也是一种鸟。我们可以通过使用两个基类来构建 Eagle 类来表达这种关系:

class Eagle : public Bird, public FlyingAnimal { ... };

从两个基类的继承都是公有的,所以派生类继承了两个接口,并且必须满足两个独立的约定。如果这两个接口都定义了同名的方法,会发生什么情况?如果该方法不是虚函数,在派生类上调用该方法会产生二义性,导致程序无法编译。如果该方法是虚函数,并且派生类提供了对应的重写版本,则不会产生二义性,将调用派生类中的方法。此外,现在 Eagle 同时是 Bird 和 FlyingAnimal:

Eagle* e = new Eagle;
Bird* b = e;
FlyingAnimal* f = e;

从派生类到基类指针的两种转换都允许,反向转换则必须使用 static_cast 或 dynamic_cast 显式进行。还有另一种有趣的转换:如果有一个指向 FlyingAnimal 类的指针,而该对象同时也属于 Bird 类,能否在这两个基类指针之间进行转换?是的,可以使用 dynamic_cast 实现:

Bird* b = new Eagle; // 也是一种 FlyingAnimal
FlyingAnimal* f = dynamic_cast<FlyingAnimal*>(b);

dynamic_cast 有时称为交叉转换 —— 不是在层次结构中向上或向下(在派生类和基类之间)进行转换,而是在层次结构的不同分支之间进行横向转换。

交叉转换也是在前一节中看到的 dynamic_cast 运行时开销较高的主要原因。尽管 dynamic_cast 最常见的用途是从 Base* 转换到 Derived*,以验证给定对象是否确实属于某个派生类,但它也可用于同一派生类的不同基类之间进行转换。

如果只是想检查一个基类对象是否真的是某个特定的派生类对象,编译器在此时是知道 Derived 类型的(不能对不完整类型使用 dynamic_cast)。因此,编译器确切地知道该派生类型包含哪些基类,并能轻易地检查你所请求的基类是否在其中。但在进行跨层次转换时,编译器只知道两个基类;而在编写这段代码时,可能还不存在同时继承这两个基类的派生类,它将在之后才编写出来。然而,编译器必须现在就生成正确的代码。所以编译器必须生成一段代码,在运行时遍历所有可能同时从这两个基类派生的类,以检查请求的类型是否在其中(实际的实现机制比这更巧妙且更高效,但要完成的任务本质相同)。

这种开销往往不必要,大多数情况下dynamic_cast 确实只是用于判断基类指针是否真正指向某个派生类对象,但这种开销并不显著。但如果对性能要求更高,则无法让 dynamic_cast 变得更快。如果需要一种快速的方式来检查一个多态对象是否属于某个特定类型,就需要使用虚函数,但还需要一个所有可能类型的列表(或至少是感兴趣的那些类型):

enum type_t { typeBase, typeDerived1, typeDerived2 };

class Base {
  virtual type_t type() const { return typeBase; }
};

class Derived1 : public Base {
  type_t type() const override { return typeDerived1; }
};

...

void process_derived1(Derived1* p);

void do_work(Base* p) {
  if (p->type() == typeDerived1) {
    process_derived1(static_cast<Derived1*>(p));
  }
}

多重继承在 C++ 中常常受到批评和排斥。这些建议中的大部分已经过时,源于早期编译器对多重继承实现得较差且效率低下的时代。使用现代编译器,这已不再是问题。人们常说多重继承会使类型层次结构更难理解和推理。或许更准确的说法是:设计一个能准确反映不同属性之间关系的多重继承层次结构更加困难,而设计不佳的层次结构自然难以理解和维护。

这些担忧主要适用于使用公有继承的层次结构,多重继承也可以私有。与使用单个私有继承相比,使用多个私有继承而非组合的理由更少。然而,空基类优化可以应用于多个空基类,如果适用的话,这仍然是使用私有继承的一个合理理由:

class Empty1 {};

class Empty2 {};

class Derived : private Empty1, private Empty2 {
  int i;
}; // sizeof(Derived) == 4

class Composed {
  int i;
  Empty1 e1;
  Empty2 e2;
}; // sizeof(Composed) == 8

多重继承在派生类表示一个结合了多个不相关、无重叠属性的系统时,可能会特别有效。在本书后续探讨各种设计模式及其 C++ 实现时,我们将遇到此类情况。