多繼承時很容易產生命名沖突,即使我們很小心地將所有類中的成員變量和成員函數都命名為不同的名字,命名沖突依然有可能發生,比如非常經典的菱形繼承層次。如下圖所示:

類A派生出類B和類C,類D繼承自類B和類C,這個時候類A中的成員變量和成員函數繼承到類D中變成了兩份,一份來自 A-->B-->D 這一路,另一份來自 A-->C-->D 這一條路。
在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數據,但大多數情況下這是多余的:因為保留多份成員變量不僅占用較多的存儲空間,還容易產生命名沖突,而且很少有這樣的需求。
為了解決這個問題,C++提供了虛基類,使得在派生類中只保留間接基類的一份成員。
聲明虛基類只需要在繼承方式前面加上 virtual 關鍵字,請看下面的例子:
#include <iostream> using namespace std; class A{ protected: int a; public: A(int a):a(a){} }; class B: virtual public A{ //聲明虛基類 protected: int b; public: B(int a, int b):A(a),b(b){} }; class C: virtual public A{ //聲明虛基類 protected: int c; public: C(int a, int c):A(a),c(c){} }; class D: virtual public B, virtual public C{ //聲明虛基類 private: int d; public: D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){} void display(); }; void D::display(){ cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; cout<<"c="<<c<<endl; cout<<"d="<<d<<endl; } int main(){ (new D(1, 2, 3, 4)) -> display(); return 0; }
本例中我們使用了虛基類,在派生類D中只有一份成員變量 a 的拷貝,所以在 display() 函數中可以直接訪問 a,而不用加類名和域解析符。
請注意派生類D的構造函數,與以往的用法有所不同。以往,在派生類的構造函數中只需負責對其直接基類初始化,再由其直接基類負責對間接基類初始化。現在,由於虛基類在派生類中只有一份成員變量,所以對這份成員變量的初始化必須由派生類直接給出。如果不由最后的派生類直接對虛基類初始化,而由虛基類的直接派生類(如類B和類C)對虛基類初始化,就有可能由於在類B和類C的構造函數中對虛基類給出不同的初始化參數而產生矛盾。所以規定:在最后的派生類中不僅要負責對其直接基類進行初始化,還要負責對虛基類初始化。
有的讀者會提出:類D的構造函數通過初始化表調了虛基類的構造函數A,而類B和類C的構造函數也通過初始化表調用了虛基類的構造函數A,這樣虛基類的構造函數豈非被調用了3次?大家不必過慮,C++編譯系統只執行最后的派生類對虛基類的構造函數的調用,而忽略虛基類的其他派生類(如類B和類C)對虛基類的構造函數的調用,這就保證了虛基類的數據成員不會被多次初始化。
最后請注意:為了保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中聲明為虛基類,否則仍然會出現對基類的多次繼承。
可以看到:使用多重繼承時要十分小心,經常會出現二義性問題。上面的例子是簡單的,如果派生的層次再多一些,多重繼承更復雜一些,程序員就很容易陷人迷 魂陣,程序的編寫、調試和維護工作都會變得更加困難。因此很多程序員不提倡在程序中使用多重繼承,只有在比較簡單和不易出現二義性的情況或實在必要時才使用多重繼承,能用單一繼承解決的問題就不要使用多重繼承。也正由於這個原因,C++之后的很多面向對象的編程語言(如Java、Smalltalk、C#、PHP等)並不支持多重繼承。
