多重基底類別

類別可以衍生自一個以上的基類。 在多重繼承模型中(其中類別衍生自多個基類),基類是使用 基表 文法元素來指定。 例如,可以指定衍生自 CollectionOfBookCollectionBook 類別宣告:

// deriv_MultipleBaseClasses.cpp
// compile with: /LD
class Collection {
};
class Book {};
class CollectionOfBook : public Book, public Collection {
    // New members
};

除了叫用建構函式和解構函式的特定案例之外,指定基類的順序並不重要。 在這些情況下,指定基底類別的順序會影響下列各項:

  • 呼叫建構函式的順序。 如果程式碼要求 BookCollectionOfBook 部分必須在 Collection 部分之前初始化,則指定的順序就很重要。 初始化會以基底清單中指定類別的順序進行。

  • 叫用解構函式進行清除的順序。 同樣地,如果類別的特定「部分」必須在其他部分終結時存在,則順序就很重要。 解構函式會以基底清單中指定的類別反向順序呼叫。

    注意

    基底類別的指定順序可能會影響類別的記憶體配置。 請不要依據記憶體中基底成員的順序做出任何程式設計上的決策。

指定 基底清單時,您無法多次指定相同的類別名稱。 不過,類別可以多次成為衍生類別的間接基底。

虛擬基底類別

由於類別可以多次做為衍生類別的間接基底類別,因此 C++ 針對此類的基底類別工作提供一種最佳化的方式。 虛擬基底類別可節省空間,並在使用多重繼承的類別階層架構時避免出現模稜兩可的問題。

每個非虛擬物件都包含基底類別中定義的資料成員。 重複項目不僅佔用空間,您還必須在存取時指定您需要的基底類別成員複本。

將基底類別指定為虛擬基底類別時,可多次將其當做間接基底,而不需要使用其資料成員的複本。 其資料成員的單一複本會由所有基底類別共用 (這些類別會將其當做虛擬基底使用)。

宣告虛擬基類時, virtual 關鍵詞會出現在衍生類別的基底清單中。

請考慮下圖中的類別階層,其說明模擬午餐行:

Diagram of a simulated lunch line.

基類為 Queue。 收銀員佇列和午餐佇列都繼承自佇列。 最後,午餐收銀員佇列繼承自收銀員佇列和午餐佇列。

模擬午餐折線圖

在圖中,QueueCashierQueueLunchQueue 的基底類別。 不過,在將這兩個類別合併為 LunchCashierQueue 時會發生下列問題:新的類別會內含兩個來自 Queue 類型的子物件,其中一個來自 CashierQueue,另一個則是來自 LunchQueue。 下圖顯示概念性記憶體配置(實際的記憶體配置可能已優化):

Diagram of a simulated lunch line object.

此圖顯示午餐收銀員佇列物件,其中包含其中兩個子物件:收銀員佇列和午餐佇列。 收銀員佇列和午餐佇列都包含 Queue 子物件。」

模擬午餐線物件

物件中有兩 QueueLunchCashierQueue 子物件。 下列程式碼會將 Queue 宣告為虛擬基底類別:

// deriv_VirtualBaseClasses.cpp
// compile with: /LD
class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

關鍵詞 virtual 可確保只包含一個子物件 Queue 複本(請參閱下圖)。

Diagram of a simulated lunch line object, with virtual base classes depicted.

此圖顯示午餐收銀員佇列物件,其中包含收銀員佇列子物件和午餐佇列子物件和午餐佇列子物件。 收銀員佇列和午餐佇列都會共用相同的 Queue 子物件。

具有虛擬基類的模擬午餐線物件

一個類別可以擁有一個虛擬元件和一個特定類型的非虛擬元件。 這發生在下圖所示的條件中:

Diagram of virtual and non virtual components of a class.

此圖表顯示佇列基類。 收銀員佇列類別和午餐佇列類別幾乎繼承自 Queue。 第三個類別「外賣佇列」會從佇列中繼承非虛擬。 午餐收銀員隊列繼承自收銀員佇列和午餐佇列。 午餐外賣收銀員隊列繼承自午餐收銀員佇列和外賣佇列。

相同類別的虛擬和非虛擬元件

在圖中,CashierQueueLunchQueue 使用 Queue 做為虛擬基底類別。 不過,TakeoutQueue 指定 Queue 做為基底類別,而不是虛擬基底類別。 因此,LunchTakeoutCashierQueue 內含兩個類型為 Queue 的子物件:一個是來自包含 LunchCashierQueue 的繼承路徑,另一個是來自包含 TakeoutQueue 的路徑。 下圖中說明此情形。

Diagram of the object layout for virtual and non virtual inheritance.

顯示午餐外賣收銀員佇列物件包含兩個子物件:外賣佇列(其中包含佇列子物件)和午餐收銀員佇列。 午餐收銀員佇列子物件包含收銀員佇列子物件和午餐佇列子物件,兩者都共用 Queue 子物件。

具有虛擬和非虛擬繼承的物件配置

注意

與使用非虛擬繼承相比較,使用虛擬繼承在大小方面提供相當大的優勢。 不過,它可能會增加額外的處理負擔。

如果衍生類別覆寫它繼承自虛擬基類的虛擬函式,而且如果衍生基類的建構函式或解構函式會使用虛擬基類的指標呼叫該函式,編譯程式可能會將其他隱藏的 “vtordisp” 字段引入具有虛擬基底的類別。 編譯 /vd0 程式選項會隱藏隱藏 vtordisp 建構函式/解構函式位移成員的新增。 編譯 /vd1 程式選項,預設值,會啟用它們所需的位置。 只有在您確定所有類別建構函式和解構函式幾乎都會呼叫虛擬函式時,才關閉 vtordisps。

編譯 /vd 程式選項會影響整個編譯模組。 vtordisp使用 pragma 以逐類別為基礎隱藏與重新允許vtordisp欄位:

#pragma vtordisp( off )
class GetReal : virtual public { ... };
\#pragma vtordisp( on )

名稱語意模糊

多重繼承實現了依循多個路徑繼承名稱的可能性。 沿著這些路徑的類別成員名稱不一定是唯一的。 這些名稱衝突稱為「模稜兩可」。

參考類別成員的所有運算式都必須進行明確參考。 下列範例將示範如何發展出模稜兩可的情況:

// deriv_NameAmbiguities.cpp
// compile with: /LD
// Declare two base classes, A and B.
class A {
public:
    unsigned a;
    unsigned b();
};

class B {
public:
    unsigned a();  // class A also has a member "a"
    int b();       //  and a member "b".
    char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

鑒於上述類別宣告,下列程式代碼模棱兩可,因為目前還不清楚 b 是否參考 b in A 或 中的 B

C *pc = new C;

pc->b();

請參考上述範例。 因為名稱a同時是類別和類別AB的成員,因此編譯程式無法辨別a要呼叫的函式。 如果成員可以參考多個函式、物件、類型或列舉程式,則對該成員的存取就是模稜兩可的情況。

編譯器會依照下列順序執行測試來偵測模稜兩可的情況:

  1. 如果對名稱的存取是模稜兩可的情況 (如上所述),則會產生錯誤訊息。

  2. 如果多載函式明確,則會加以解析。

  3. 如果對名稱的存取違反成員存取的權限,則會產生錯誤訊息 (如需詳細資訊,請參閱Member-存取控制.)

當運算式透過繼承產生模稜兩可的情況時,您可以使用類別名稱限定所指的名稱,藉此手動解析該名稱。 若要讓上述範例正確編譯而不發生模稜兩可的情況,請使用如下所示的程式碼:

C *pc = new C;

pc->B::a();

注意

C 宣告時,它可能會在 BC 的範圍中參考時造成錯誤。 不過,只有 B 範圍中實際參考未限定的 C 時,才會發出錯誤。

支配

可以透過繼承圖形到達多個名稱(函式、物件或列舉值)。 這種情況視為與非虛擬基底類別模稜兩可。 虛擬基類也模棱兩可,除非其中一個名稱「主宰」其他名稱。

如果名稱在類別中定義,且其中一個類別衍生自另一個類別,則名稱會主宰另一個名稱。 主要名稱是衍生類別中的名稱,這個名稱會在出現模稜兩可時使用,如下列範例所示:

// deriv_Dominance.cpp
// compile with: /LD
class A {
public:
    int a;
};

class B : public virtual A {
public:
    int a();
};

class C : public virtual A {};

class D : public B, public C {
public:
    D() { a(); } // Not ambiguous. B::a() dominates A::a.
};

模稜兩可的轉換

從指標或參考明確或隱含轉換為類別類型,可能會造成模稜兩可的情況。 下圖「指標不明確地轉換為基底類別」顯示下列項目:

  • 宣告類型為 D 的物件。

  • 將運算子位址 (&) 套用至該物件的效果。 運算子的位址一律會提供 物件的基位址。

  • 將使用傳址運算子取得的指標明確轉換為基底類別類型 A 的作用。 強制將物件的地址強制設定為類型 A* ,不一定會為編譯程式提供足夠的資訊,以便選取類型的子物件 A ;在此情況下,有兩個子物件存在。

Diagram showing how the conversion of pointers to base classes can be ambiguous.

圖表會先顯示繼承階層:A 是基類。 B 和 C 繼承自 A。D 繼承自 B 和 C。然後,會顯示物件 D 的記憶體配置。D 中有三個子物件:B(包括子物件 A)和 C(包括子物件 A)。 程序代碼和 d 指向子物件 B 中的 A。程序代碼 (* A) 和 d 指向子物件 B 和子物件 C。

指標到基類的模棱兩可轉換

轉換成類型 A* (指向的指標 A)模棱兩可,因為無法辨別類型的子對像是正確的子物件 A 。 您可以藉由明確指定要使用的子物件來避免模棱兩可,如下所示:

(A *)(B *)&d       // Use B subobject.
(A *)(C *)&d       // Use C subobject.

語意模糊和虛擬基底類別

如果使用虛擬基底類別,則可以透過多重繼承路徑存取函式、物件、類型和列舉值。 因為基類只有一個實例,因此存取這些名稱時沒有模棱兩可。

下圖顯示物件使用虛擬和非虛擬繼承的組成方式。

Diagram showing virtual derivation and nonvirtual derivation.

圖表會先顯示繼承階層:A 是基類。 B 和 C 幾乎繼承自 A。D 幾乎繼承自 B 和 C。然後,會顯示 D 的配置。 D 包含子物件 B 和 C,其共用子物件 A。然後配置會顯示為使用非虛擬繼承衍生的相同階層。 在此情況下,D 包含子物件 B 和 C。B 和 C 都包含自己的子物件 A 複本。

虛擬和非虛擬衍生

在圖中,透過非虛擬基底類別存取類別 A 的所有成員會產生模稜兩可的情況,編譯器不會提供是否要使用與 B 關聯的子物件,或使用與 C 關聯之子物件的資訊。 不過,當 指定為虛擬基類時 A ,就沒有任何問題要存取哪個子物件。

另請參閱

繼承