多重继承

物件導向程式設計中的多重繼承(英語: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

外部链接

參見