870920 Menu

SwingCoder之C++备忘录·13

多重继承

又称为“多基派生”。语法格式:

class 派生类 :继承方式 基类1,继承方式 基类2,…
{ // … };

1、格式。写多重继承类时,如果所继承的基类之前不使用继承方式关键字(权限标识符前缀),则默认为private继承。清晰起见,建议显式给出private继承方式关键字,并将多重继承的基类分行书写。示例:
class MainHostWindow : public DocumentWindow,
public MenuBarModel,
public ApplicationCommandTarget,
protected ChangeListener,
private FileDragAndDropTarget
{ // … };

2、二义性。派生类使用基类的成员时,要避免二义性,即不同基类中相同的成员名称。如果确实无法避免,派生类对象调用成员时要前缀基类类名和作用域标识符。比如:类Z继承自类X和类Y,X和Y中均有f()这个成员函数,类Z的对象obj调用f()函数时,就必须使用作用域前缀:
obj->X::f(); // 指明调用基类X的f()
obj->Y::f(); // 指明调用基类Y的f()

3、初始化。派生类的构造函数需初始化基类,在构造函数的初始化列表中完成。因此,多重继承的派生类也许需要很多构造参数,一部分用来初始化不同的基类,一部分用来初始化自身的数据成员。即使派生类自身无需构造参数,其基类也必须要进行初始化,此时,派生类依然需要构造参数,除非基类本身也无需构造参数,或者可以直接赋值。比如(基类的构造函数直接赋值,而非派生类的构造参数转交给基类的构造函数):
// MainHostWindow类的构造函数。语法:构造函数名 : 初始化列表 { 函数体 }
MainHostWindow() : DocumentWindow (
JUCEApplication::getInstance()->getApplicationName(),
Colours::lightgrey, DocumentWindow::allButtons)
{ //… }
派生类中初始化基类,相当于构造基类,即给基类的数据成员做初始化,使之完整可用,这一步是必须的。否则,不完整的基类被派生类使用,派生类本身也是不完整的,编译时会报错。

4、构造顺序:多个基类的初始化顺序与派生类构造函数初始化列表中的顺序无关,而与派生类声明时的继承顺序有关。派生类实例化对象时,先按继承顺序构造所有基类,而后初始化本类的数据成员(尽量在初始化列表中给出本类数据成员的初始化语句,而避免在构造函数中赋值),最后运行构造函数中的其他语句。

5、析构顺序:与构造顺序相反。先析构派生类,而后逐次析构基类。即使派生类不定义析构函数,也照此执行,互不影响,这一点与构造时有所不同。在使用基类指针而实际指向子类的堆对象时,为保证类系中各层析构函数正确执行,各基类的析构函数要显式声明为virtual函数。

6、避免“钻石型”多重继承,避免虚继承。如果所继承的基类接口无需公开,并且不再派生子类,则使用private继承之。如果仍需派生子类,则使用protected继承。private和protected继承与类的组合一样,均属“has-a”关系,而public继承则是典型的“is-a”关系。

7、关于虚继承。又称为“虚基类”,可避免继承多个具有相同基类的类时出现多个祖先类的副本,示例:
class Singer : virtual public Worker; // 中间类Singer使用了虚继承
class Waiter : virtual public Worker; // 中间类Waiter也使用了虚继承
// 类SingerWaiter多重继承自Singer和Waiter
class SingerWaiter : public Singer, public Waiter;

这样一来,SingerWaiter类只包含一个Worker类的副本,而不是两个(因为Singer类和Waiter类均继承自Worker类)。也就是说,要将SingerWaiter类加入Worker类系中,其基类Singer和Waiter这两个类必须采用虚继承的方式声明和定义。这是第一点要注意的。

第二点要注意的是:采用虚继承时,SingerWaiter类需在构造函数的初始化列表中加入对祖先类Worker的初始化语句,这种规则,对非虚继承来说是违例的,但是虚继承的情况下,却是必须的:

SingerWaiter (1参,2参,3参) : Worker(1参), Singer(2参), Waiter(3参)
{ // … }

当然,如果祖先类Worker有默认的构造函数,则可以省略初始化列表中对祖先类的初始化语句。

第三点要注意的:非虚继承且多重继承的底层类调用拥有相同基类的基类的成员函数时,需使用作用域运算符,指明所调用的是哪个基类的成员函数,此即解决二义性的问题。但是,虚继承使用优先规则来自动处理二义性,即优先使用最底层派生类的同名成员函数。不过,此时仍有可能出现平级类的二义性问题,解决方案同非虚继承类。