C++学习笔记十九-多重继承与虚继承
概述:多重继承是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性。
一、多重继承的定义
1. 由逗号分隔的基类列表:
class Panda : public Bear, public Endangered { };
派生类为每个基类(显式或隐式地)指定了访问级别——public、protected 或 private。像单继承一样,只有在定义之后,类才可以用作多重继承的基类。对于类可以继承的基类的数目,没有语言强加强加的限制,但在一个给定派生列表中,一个基类只能出现一次。
2.多重继承的派生类从每个基类中继承状态:在多重继承下,派生类的对象包含每个基类的基类子对象。
3.派生类构造函数初始化所有基类:构造派生类型的对象包括构造和初始化所有基类子对象。像继承单个基类的情况一样,派生类的构造函数可以在构造函数初始化式中给零个或多个基类传递值。多重继承中若没有显式调用某个基类的构造函数,则编译器会调用该基类默认构造函数
4.构造的次序:构造函数初始化式只能控制用于初始化基类的值,不能控制基类的构造次序。基类构造函数按照基类构造函数在类派生列表中的出现次序调用。
5.构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中的出现次序的影响。
6.析构的次序:总是按构造函数运行的逆序调用析构函数。
二、转换与多个基类
1.在单个基类情况下,派生类的指针或引用可以自动转换为基类的指针或引用,对于多重继承也是如此,派生类的指针或引用可以转换为其任意其类的指针或引用。
2.在多重继承情况下,遇到二义性转换的可能性更大。编译器不会试图根据派生类转换来区别基类间的转换,转换到每个基类都一样好。例如,如果有 print 函数的重载版本:
void print(const Bear&); void print(const Endangered&);
三、多重继承下的虚函数
1.像单继承一样,用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引入的成员。
2.当一个类继承于多个基类的时候,那些基类之间没有隐含的关系,不允许使用一个基类的指针访问其他基类的成员。
3.确定使用哪个虚析构函数:假定所有根基类都将它们的析构函数适当定义为虚函数,那么,无论通过哪种指针类型删除对象,虚析构函数的处理都是一致的。
四、多重继承派生类的复制控制:
1.多重继承的派生类的逐个成员初始化、赋值和析构,表现得与单继承下的一样,使用基类自己的复制构造函数、赋值操作符或析构函数隐式构造、赋值或撤销每个基类。
2.像单继承的情况一样,如果具有多个基类的类定义了自己的析构函数,该析构函数只负责清除派生类。如果派生类定义了自己的复制构造函数或赋值操作符,则类负责复制(赋值)所有的基类子部分。只有派生类使用复制构造函数或赋值操作符的合成版本,才自动复制或赋值基类部分。
五、多重继承下的类作用域:
1.当一个类有多个基类的时候,通过所有直接基类同时进行名字查找。多重继承的派生类有可能从两个或多个基类继承同名成员,对该成员不加限定的使用是二义性的。
2.即使两个继承的函数有不同的形参表,也会产生错误。类似地,即使函数在一个类中是私有的而在另一个类中是公用或受保护的,也是错误的。
3.避免用户二义性:可以通过指定使用哪个类解决二义性:
ying_yang.Endangered::print(cout);
4.避免潜在二义性最好的方法是,在解决二义性的派生类中定义函数的一个版本。例如,应该给选择使用哪个 print 版本的 Panda 类一个 print 函数:
std::ostream& Panda::print(std::ostream &os) const { Bear::print(os); // print the Bear part Endangered::print(os); // print the Endangered part return os; }
六、虚继承:在多重继承下,一个基类可以在派生层次中出现多次。实际上,我们的程序已经使用过通过继承层次多次继承同一基类的类。
1.在 C++ 中,通过使用虚继承解决这类问题。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
2.istream 和 ostream 类对它们的基类进行虚继承。通过使基类成为虚基类,istream 和 ostream 指定,如果其他类(如 iostream 同时继承它们两个,则派生类中只出现它们的公共基类的一个副本。通过在派生列表中包含关键字 virtual 设置虚基类:
class istream : public virtual ios { ... }; class ostream : virtual public ios { ... }; // iostream inherits only one copy of its ios base class class iostream: public istream, public ostream { ... }; 3.虚基类的声明:通过用关键字 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 { /* ... */ }; 4.指定虚派生只影响从指定了虚基类的类派生的类。除了影响派生类自己的对象之外,它也是关于派生类与自己的未来派生类的关系的一个陈述。virtual 说明符陈述了在后代派生类中共享指定基类的单个实例的愿望。
5.任何可被指定为基类的类也可以被指定为虚基类,虚基类可以包含通常由非虚基类支持的任意类元素。
6.支持到基类的常规转换:即使基类是虚基类,也照常可以通过基类类型的指针或引用操纵派生类的对象。
七、虚基类成员的可见性
1. 假定通过多个派生路径继承名为 X 的成员,有下面三种可能性:
a. 如果在每个路径中 X 表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例。
b.如果在某个路径中 X 是虚基类的成员,而在另一路径中 X 是后代派生类的成员,也没有二义性——特定派生类实例的优先级高于共享虚基类实例
c. 如果沿每个继承路径 X 表示后代派生类的不同成员,则该成员的直接访问是二义性的。
像非虚多重继承层次一样,这种二义性最好用在派生类中提供覆盖实例的类来解决。
八、特殊的初始化语义
1.通常,每个类只初始化自己的直接基类。在应用于虚基类的进修,这个初始化策略会失败。如果使用常规规则,就可能会多次初始化虚基类。类将沿着包含该虚基类的每个继承路径初始化。为了解决这个重复初始化问题,从具有虚基类的类继承的类对初始化进行特殊处理。在虚派生中,由最低层派生类的构造函数初始化虚基类。
2.虽然由最低层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般也必须为该基类提供自己的初始化式。只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化式只有创建中间类型的对象时使用。
九、怎样构造虚继承的对象
1.无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。
a.按声明次序检查直接基类,确定是否存在虚基类。一旦构造了虚基类,就按声明次序调用非虚基类的构造函数.
b.在合成复制构造函数中使用同样的构造次序,在合成赋值操作符中也是按这个次序给基类赋值。保证调用基类析构函数的次序与构造函数的调用次序相反。