多重继承

本页使用了标题或全文手工转换
维基百科,自由的百科全书

面向对象编程中的多重继承(英语:multiple inheritance缩写MI)指的是一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承指一个类别只可以继承自一个父类。

争议[编辑]

多重继承可以导致某些令人混淆的情况,所以关于它的好处与风险之间孰轻孰重常常受人争论。Java使用了一个折衷的办法:Java允许一个类别继承自多于一个父接口(可以指定某一个类别,它继承了所有父类的类型,并必须拥有所有父类接口的外部可见方法的具体实现,并允许编译器强制以上要求),但只可以从一个父类继承实现(方法与数据)。微软的.NET编程语言,例如C#Visual Basic .NET也使用了这种接口的做法。

面向对象的程式设计中,继承描述了两种类型或两个类的物件,其中一种是另外一种的“子类型”或“子类”。子类继承了父类的特征,允许分享功能。例如,可以创造一个“哺乳类动物”类别,拥有进食、繁殖等的功能;然后定义一个子类型“猫”,它可以从父类继承上述功能,不需重新编写程序,同时增加属于自己的新功能,例如“追赶老鼠”。

然而,如果想同时自多于一个结构继承,例如容许“猫”继承“哺乳类动物”之余,同时继承“卡通角色”和“宠物”,缺乏多重继承往往会导致十分笨拙的混合继承,或迫使同一个功能在多于一个地方被重写。(这带来了维护上的问题)

多年以来,多重继承都是一个敏感的话题,反对者指它增加了程序的复杂性与含糊性,例如在钻石问题(或称菱型缺陷)中。Loki函式库针对多重继承进行改良,以TypeList(二叉树结构)避免这个问题。

各种编程语言有不同的方式处理上述问题。例如Eiffel容许子类型透过重命名,或提前为他们确定选择规则,来适应adapt)它继承得来的功能。Java允许物件从多个接口继承,但仅允许一个实现继承。REALbasic与它相似,并增加了一个不需使用继承来“扩展”一个类别的功能。Perl使用一种有序列表式的继承机制:搜索方法时,它会先搜索当前类别的方法,然后使用深度优先搜索来顺序查找各个继承类别及其父类。CLOS允许程式设计者完全控制方法的组合。如果这还不足够,元对象协议给程式设计者一种手段去修改继承,方法调度类别特例化,及其它内部的机制,而不影响系统的稳定性。

C++与多继承[编辑]

C++支持多重继承,允许对现实世界进行更直接的建模,Borland C++OWL Framework大量使用多重继承来描述视窗的关系。微软的MFC仅使用单一继承描述视窗,ATL使用多重继承实现COM/ActiveXWTL则使用多重继承实现视窗。

多重继承与被覆盖的虚函数[编辑]

对于最左基类,虚函数的覆盖与单继承情形一致。

对于非最左的基类,虚函数仍然可能会被派生类的成员函数覆盖。

成员函数中this指针调整[编辑]

一个类的非静态成员函数,一般需要使用类对象的this指针来访问类数据成员。程序加载到内存后,成员函数代码占据了一块内存空间。成员函数并不知道自身是作为一个单独的(或最派生)类的直接成员函数,还是作为一个被派生的基类的成员函数而存在。实际上在内存空间的非静态成员函数,可能会同时是单独的(或最派生)类的直接成员函数与被派生的基类的成员函数。非静态成员函数也仅知道声明了该函数的类的数据成员的空间分布,不可能知道以该类为基类的派生类的数据成员的空间分布。因此调用非静态成员函数时,调用者有责任传给成员函数正确的this指针,即令this指针指向声明了该成员函数的类的对象开始地址。

对于单继承,派生类与基类的对象开始地址是一样的,因此调用非静态成员函数不需要调整this指针。对于多继承,调用不是最左基类的非静态成员函数时,调用者必须先调整this指针。这又分为两种情形:

一是非虚函数,在函数调用现场直接调整this的值。这是编译器根据多重继承的派生类的实例对象或指针在编译时就能确定的。例如:

struct base1{
   int v1;
   void foo1(int){} 
}
struct base2{
   int v2;
   void foo2(int){} 
}
struct derive: base1,base2{
};
derive d;
int main()
{    
    derive *p=&d;
    d.foo2(101);
    /* 上述调用语句编译后为:
push        65h                    ;参数101压栈
lea         ecx,offset d+4         ;根据thiscall调用协议,ecx保存了this的值
call        base2::foo2 (1181145h)
*/
    p->foo2(102);
}

二是虚函数情形。因为虚函数的开始地址必须存放在虚表条目中,所以多重继承的派生类对非最左基类的被覆盖(override)的虚函数,在该派生类的相应的虚表条目中填写的是一个桩(thunk)地址。该桩通常只有两条机器指令,首先是调整this值(即修改ecx寄存器),然后是调用指令(call)。

参考文献[编辑]

  • Andrei Alexandrescu. Modern C++ Design

外部链接[编辑]

参见[编辑]