大多数应用程序使用单个基类的公用继承,但是在某些情况下,需要从多于一个直接基类派生类,也就是所谓的多重继承,多重继承的派生类继承其所有父类的属性。
1:多重继承的例子:
class Bear : public ZooAnimal { }; class Panda : public Bear, public Endangered { };
派生类为每个基类(显式或隐式地)指定了访问级别——public、protected 或private。
2:在多重继承下,派生类的对象包含每个基类的基类子对象。当构造一个Panda对象的时候,该对象包含一个 Bear 类子对象(Bear 类子对象本身包含一个 ZooAnimal 基类子对象)、一个 Endangered 类子对象以及 Panda 类中声明的非 static 数据成员(如果有的话),如下图:
3:构造派生类的对象包括构造和初始化所有基类子对象。派生类的构造函数可以在构造函数初始化式中给零个或多个基类传递值:
// explicitly initialize both base classes Panda::Panda(std::string name, bool onExhibit) : Bear(name, onExhibit, "Panda"),Endangered(Endangered::critical) { } // implicitly use Bear default constructor to initialize the Bear subobject Panda::Panda(): Endangered(Endangered::critical) { }
构造函数初始化式只能控制用于初始化基类的值,不能控制基类的构造次序。基类构造函数按照基类在类派生列表中的出现次序调用。对 Panda 而言,基类初始化的次序是:
a. ZooAnimal,从 Panda 的直接基类 Bear 沿层次向上的最终基类。
b. Bear,第一个直接基类。
c. Endangered,第二个直接基类,它本身没有基类。
d. Panda,初始化 Panda 本身的成员,然后运行它的构造函数的函数体。
总是按构造函数运行的逆序调用析构函数。在我们的例子中,调用析构函数的次序是 ~Panda, ~Endangered, ~Bear, ~ZooAnimal。
4:在多重继承情况下,遇到二义性转换的可能性更大。例如,如果有 print 函数的重载版本:
void print(const Bear&); void print(const Endangered&); Panda ying_yang("ying_yang"); print(ying_yang); // error: ambiguous
导致一个编译时错误,指出该调用是二义性的。
5:假定所有根基类都将它们的析构函数适当定义为虚函数,那么,无论通过哪种指针类型删除对象,虚析构函数的处理都是一致的:
// each pointer points to a Panda delete pz; // pz is a ZooAnimal* delete pb; // pb is a Bear* delete pp; // pp is a Panda* delete pe; // pe is a Endangered*
假定这些指针每个都向 Panda 对象,则每种情况下发生完全相同的析构函数调用次序。析构函数调用的次序是构造函数次序的逆序:通过虚机制调用 Panda 析构函数。随着 Panda 析构函数的执行,依次调用 Endangered、Bear 和ZooAnimal 的析构函数。
像单继承的情况一样,如果具有多个基类的类定义了自己的析构函数,该析构函数只负责清除派生类。如果派生类定义了自己的复制构造函数或赋值操作符,则类负责复制(赋值)所有的基类子部分。只有派生类使用复制构造函数或赋值操作符的合成版本,才自动复制或赋值基类部分。
6:在多重继承下,名字查找同时检察所有的基类继承子树——在我们的例子中,并行查找Endangered 子树和 Bear/ZooAnimal 子树。如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的。
假定 Bear 类和 Endangered 类都定义了名为 print 的成员,如果 Panda 类没有定义该成员,则ying_yang.print(cout);这样的语句将导致编译时错误。
派生只是导致潜在的二义性,如果没有 Panda 对象调用 print,就可以避免这个二义性。如果每个 print 调用明确指出想要哪个版本——Bear::print 还是Endangered::print,也可以避免错误。只有在存在使用该成员的二义性尝试的时候,才会出错。
即使两个继承的函数有不同的形参表,也会产生错误。类似地,即使函数在一个类中是私有的而在另一个类中是公用或受保护的,也是错误的。
可以通过指定使用哪个类解决二义性:ying_yang.Endangered::print(cout);
7:在多重继承下,一个基类可以在派生层次中出现多次。比如IO 库类:
多重继承的类从它的每个父类继承状态和动作,如果 IO 类型使用常规继承,则每个 iostream 对象可能包含两个 ios 子对象:一个包含在它的 istream 子对象中,另一个包含在它的 ostream 子对象中,从设计角度讲,这个实现正是错误的。
使用虚继承解决这类问题。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。
istream 和 ostream 类对它们的基类进行虚继承。通过使基类成为虚基类,istream 和 ostream 指定,如果其他类(如 iostream )同时继承它们两个,则派生类中只出现它们的公共基类的一个副本。通过在派生列表中包含关键字virtual 设置虚基类:
class istream : public virtual ios { ... }; class ostream : virtual public ios { ... }; class iostream: public istream, public ostream { ... };
8:考虑下面的例子:
class Panda : public Bear, public Raccoon, public Endangered { };
必须在提出虚派生的任意实际需要之前进行虚派生(在例中,Bear类和 Raccoon 类的虚派生)。只有在使用 Panda 的声明时,虚继承才是必要的。
通过用关键字 virtual 修改声明,将基类指定为通过虚继承派生。例如,下面的声明使 ZooAnimal 类成为 Bear 类和 Raccoon 类的虚基类:
// the order of the keywords public and virtual is not significant class Raccoon : public virtual ZooAnimal { /* ... */ }; class Bear : virtual public ZooAnimal { /* ... */ };
9:假定通过多个派生路径继承名为 X 的成员,有下面三种可能性:
a. 如果在每个路径中 X 表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例。
b. 如果在某个路径中 X 是虚基类的成员,而在另一路径中 X 是后代派生类的成员,也没有二义性——特定派生类实例的优先级高于共享虚基类实例。
c. 如果沿每个继承路径 X 表示后代派生类的不同成员,则该成员的直接访问是二义性的。
10:每个类只初始化自己的直接基类。在应用于虚基类的情况,这个初始化策略会失败。如果使用常规规则,就可能会多次初始化虚基类。类将沿着包含该虚基类的每个继承路径初始化。在 ZooAnimal 示例中,使用常规规则将导致 Bear类和 Raccoon 类都试图初始化 Panda 对象的 ZooAnimal 类部分。
为了解决这个重复初始化问题,从具有虚基类的类继承的类对初始化进行特殊处理。在虚派生中,由最低层派生类的构造函数初始化虚基类。在我们的例子中,当创建 Panda 对象的时候,只有 Panda 构造函数控制怎样初始化 ZooAnimal基类。
虽然由最低层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般也必须为该基类提供自己的初始化式。只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化式只有创建中间类型的对象时使用。
在我们的层次中,可以有 Bear、Raccoon 或 Panda 类型的对象。创建 Panda 对象的时候,它是最低层派生类型并控制共享的 ZooAnimal 基类的初始化:创建Bear 对象(或 Raccoon 对象)的时候,不涉及更低层的派生类型。在这种情况下,Bear(或 Raccoon)构造函数像平常一样直接初始化它们的 ZooAnimal 基类:
Bear::Bear(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Bear") { } Raccoon::Raccoon(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Raccoon") { }
虽然 ZooAnimal 不是 Panda 的直接基类,但是 Panda 构造函数也初始化ZooAnimal 基类:
Panda::Panda(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Panda"), Bear(name, onExhibit), Raccoon(name, onExhibit), Endangered(Endangered::critical), sleeping_flag(false) { } Bear winnie("pooh"); // Bear constructor initializes ZooAnimal Raccoon meeko("meeko"); // Raccoon constructor initializes ZooAnimal Panda yolo("yolo"); // Panda constructor initializes ZooAnimal
当创建 Panda 对象的时候,
首先使用构造函数初始化列表中指定的初始化式构造 ZooAnimal 部分;接下来,构造 Bear 部分。忽略 Bear 的用于 ZooAnimal 构造函数初始化列表的初始化式;然后,构造 Raccoon 部分,再次忽略 ZooAnimal 初始化式;最后,构造 Panda 部分。
11:无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。
下面TeddyBear的派生中,有两个虚基类:ToyAnimal基类和派生 Bear 的间接基类 ZooAnimal:
class Character { /* ... */ }; class BookCharacter : public Character { /* ... */ }; class ToyAnimal { /* ... */ }; class TeddyBear: public BookCharacter, public Bear, public virtual ToyAnimal { /* ... */ };
TeddyBear 的虚基类的构造次序是先 ZooAnimal 再 ToyAnimal。一旦构造了虚基类,就按声明次序调用非虚基类的构造函数:首先是 BookCharacter,它导致调用 Character 构造函数,然后是 Bear。因此,为了创建 TeddyBear 对象,按下面次序调用构造函数:
ZooAnimal(); // Bear‘s virtual base class ToyAnimal(); // immediate virtual base class Character(); // BookCharacter‘s nonvirtual base class BookCharacter(); // immediate nonvirtual base class Bear(); // immediate nonvirtual base class TeddyBear(); // most derived class
在这里,由最低层派生类 TeddyBear 指定用于 ZooAnimal 和 ToyAnimal 的初始化式。
在合成复制构造函数中使用同样的构造次序,在合成赋值操作符中也是按这个次序给基类赋值。保证调用基类析构函数的次序与构造函数的调用次序相反。