C 中虛繼承

2021-10-13 21:29:03 字數 3991 閱讀 6295

在c++中多繼承時很容易產生命名衝突,即使我們很小心地將所有類中的成員變數和成員函式都命名為不同的名字,命名衝突依然有可能發生,比如典型的是菱形繼承,如下圖所示:

類 a 派生出類 b 和類 c,類 d 繼承自類 b 和類 c,這個時候類 a 中的成員變數和成員函式繼承到類 d 中變成了兩份,乙份來自 a-->b-->d 這條路徑,另乙份來自 a-->c-->d 這條路徑。

在乙個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變數中分別存放不同的資料,但大多數情況下這是多餘的:因為保留多份成員變數不僅占用較多的儲存空間,還容易產生命名衝突。假如類 a 有乙個成員變數 a,那麼在類 d 中直接訪問 a 就會產生歧義,編譯器不知道它究竟來自 a -->b-->d 這條路徑,還是來自 a-->c-->d 這條路徑。

例:菱形繼承

//間接基類a

class a;

//直接基類b

class b: public a;

//直接基類c

class c: public a;

//派生類d

class d: public b, public c //命名衝突

void setb(int b) //正確

void setc(int c) //正確

void setd(int d) //正確

private:

int m_d;

};int main()

這段**實現了上圖所示的菱形繼承,第 25 行**試圖直接訪問成員變數 m_a,結果發生了錯誤,因為類 b 和類 c 中都有成員變數 m_a(從 a 類繼承而來),編譯器不知道選用哪乙個,所以產生了歧義。

為了消除歧義,我們可以在 m_a 的前面指明它具體來自哪個類:

void seta(int a)	// b類中的m_a

void seta(int a) // c類中的m_a

為了解決多繼承時的命名衝突和冗餘資料問題,

c++

提出了虛繼承,使得在派生類中只保留乙份間接基類的成員。

例:虛繼承

//間接基類a

class a;

//直接基類b

class b: virtual public a;

//直接基類c

class c: virtual public a;

//派生類d

class d: public b, public c //正確

void setb(int b) //正確

void setc(int c) //正確

void setd(int d) //正確

private:

int m_d;

};int main()

這段**使用虛繼承重新實現了上圖所示的菱形繼承,這樣在派生類 d 中就只保留了乙份成員變數 m_a(a::m_a),直接訪問就不會再有歧義了。

虛繼承的目的是讓某個類做出宣告,承諾願意共享它的基類。其中,這個被共享的基類就稱為虛基類(virtual base class),本例中的 a 就是乙個虛基類。在這種機制下,不論虛基類在繼承體系**現了多少次,在派生類中都只包含乙份虛基類的成員。

本例的虛繼承關係如下:

觀察這個新的繼承體系,我們會發現虛繼承的乙個不太直觀的特徵:必須在虛派生的真實需求出現前就已經完成虛派生的操作。在上圖中,當定義 d 類時才出現了對虛派生的需求,但是如果 b 類和 c 類不是從 a 類虛派生得到的,那麼 d 類還是會保留 a 類的兩份成員。

換個角度講,虛派生只影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。

c++標準庫中的 iostream 類就是乙個虛繼承的實際應用案例。iostream 從 istream 和 ostream 直接繼承而來,而 istream 和 ostream 又都繼承自乙個共同的名為 base_ios 的類,是典型的菱形繼承。此時 istream 和 ostream 必須採用虛繼承,否則將導致 iostream 類中保留兩份 base_ios 類的成員。

因為在虛繼承的最終派生類中只保留了乙份虛基類的成員,所以該成員可以被直接訪問,不會產生二義性。此外,如果虛基類的成員只被一條派生路徑覆蓋,那麼仍然可以直接訪問這個被覆蓋的成員。但是如果該成員被兩條或多條路徑覆蓋了,那就不能直接訪問了,此時必須指明該成員屬於哪個類。

以圖2中的菱形繼承為例,假設 a 定義了乙個名為 x 的成員變數,當我們在 d 中直接訪問 x 時,會有三種可能性:

如果 b 和 c 中都沒有 x 的定義,那麼 x 將被解析為 a 的成員,此時不存在二義性。

如果 b 或 c 其中的乙個類定義了 x,也不會有二義性,派生類的 x 比虛基類的 x 優先順序更高。

如果 b 和 c 中都定義了 x,那麼直接訪問 x 將產生二義性問題。

在虛繼承中,虛基類是由最終的派生類初始化的,換句話說,最終派生類的建構函式必須要呼叫虛基類的建構函式。對最終的派生類來說,虛基類是間接基類,而不是直接基類。這跟普通繼承不同,在普通繼承中,派生類建構函式中只能呼叫直接基類的建構函式,不能呼叫間接基類的。

例:虛繼承時的建構函式的初始化

#include using namespace std;

//虛基類a

class a;

a::a(int a): m_a(a)

//直接派生類b

class b: virtual public a;

b::b(int a, int b): a(a), m_b(b)

void b::display()

在最終派生類 d 的建構函式中,除了呼叫 b 和 c 的建構函式,還呼叫了 a 的建構函式,這說明 d 不但要負責初始化直接基類 b 和 c,還要負責初始化間接基類 a。而在以往的普通繼承中,派生類的建構函式只負責初始化它的直接基類,再由直接基類的建構函式初始化間接基類,使用者嘗試呼叫間接基類的建構函式將導致錯誤。

現在採用了虛繼承,虛基類 a 在最終派生類 d 中只保留了乙份成員變數 m_a,如果由 b 和 c 初始化 m_a,那麼 b 和 c 在呼叫 a 的建構函式時很有可能給出不同的實參,這個時候編譯器就會犯迷糊,不知道使用哪個實參初始化 m_a。

為了避免出現這種矛盾的情況,c++ 乾脆規定必須由最終的派生類 d 來初始化虛基類 a,直接派生類 b 和 c 對 a 的建構函式的呼叫是無效的。在第 50 行**中,呼叫 b 的建構函式時試圖將 m_a 初始化為 90,呼叫 c 的建構函式時試圖將 m_a 初始化為 100,但是輸出結果有力地證明了這些都是無效的,m_a 最終被初始化為 50,這正是在 d 中直接呼叫 a 的建構函式的結果。

另外需要關注的是建構函式的執行順序。虛繼承時建構函式的執行順序與普通繼承時不同:在最終派生類的構造函式呼叫列表中,不管各個建構函式出現的順序如何,編譯器總是先呼叫虛基類的建構函式,再按照出現的順序呼叫其他的建構函式;而對於普通繼承,就是按照建構函式出現的順序依次呼叫的。

另:虛繼承時建構函式出現順序:

// 原:

d::d(int a, int b, int c, int d): a(a), b(90, b), c(100, c), m_d(d)

// 改:

d::d(int a, int b, int c, int d): b(90, b), c(100, c), a(a), m_d(d)

雖然我們將 a() 放在了最後,但是編譯器仍然會先呼叫 a(),然後再呼叫 b()、c(),因為 a() 是虛基類的建構函式,比其他建構函式優先順序高。如果沒有使用虛繼承的話,那麼編譯器將按照出現的順序依次呼叫 b()、c()、a()。

C 中虛繼承

虛繼承和虛函式是完全無相關的兩個概念。虛繼承是解決c 多重繼承問題的一種手段,從不同途徑繼承來的同一基類,會在子類中存在多份拷貝。這將存在兩個問題 其一,浪費儲存空間 第二,存在二義性問題,通常可以將派生類物件的位址賦值給基類物件,實現的具體方式是,將基類指標指向繼承類 繼承類有基類的拷貝 中的基類...

c 中菱形繼承 虛繼承

關於菱形繼承 相當於在c 中,分別建立四個類,動物類,羊類,駝類,羊駝類,繼承關係如圖所示。在類中只建立乙個屬性,年齡。動物類 class animal 羊類 class sheep virtualpublic animal 駝類 class tuo virtualpublic animal 羊駝類...

C 虛繼承和虛繼承

虛繼承是在多繼承中為了解決衝突而技術。學術一點來說,是指乙個指定的基類,在繼承體系結構中,將其成員資料例項共享給也從這個基類直接或間接派生的其他類。虛繼承非常有用,可以避免多繼承的歧義和多重拷貝。考慮有如下繼承結構。b和c繼承a,d多繼承b c,我們看以下 class a class b publi...