技術(shù)交流QQ群:1027579432,歡迎你的加入!
1.Cpp中的虛繼承與虛基類
-
在多繼承時(shí),很容易產(chǎn)生命名沖突的問(wèn)題,即使我們很小心地將所有類中的成員變量和成員函數(shù)都命名為不同的名字,命名沖突依然有可能發(fā)生,比如典型的是菱形繼承,如下圖所示:
菱形繼承 - 類A派生出類B和類C,類D繼承自類B和類C,這個(gè)時(shí)候類A中的成員變量和成員函數(shù)繼承到類D中變成了兩份,一份來(lái)自A-->B-->D這條路徑,另一份來(lái)自A-->C-->D這條路徑。在一個(gè)派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數(shù)據(jù),但大多數(shù)情況下這是多余的:因?yàn)楸A舳喾莩蓡T變量不僅占用較多的存儲(chǔ)空間,還容易產(chǎn)生命名沖突。假如類A有一個(gè)成員變量a,那么在類D中直接訪問(wèn)a 就會(huì)產(chǎn)生歧義,編譯器不知道它究竟來(lái)自A -->B-->D這條路徑,還是來(lái)自A-->C-->D這條路徑。下面是菱形繼承的具體實(shí)現(xiàn):
#include "iostream" using namespace std; // 間接基類 class A { protected: int m_a; }; // 直接基類B class B : public A { protected: int m_b; }; // 直接基類C class C : public A { protected: int m_c; }; // 派生類D class D : public B, public C { public: // void seta(int a) { m_a = a; } 命名沖突,為了解決命名沖突,可以使用void B::seta(int a){m_a = a;} void setb(int b) { m_b = b; } void setc(int c) { m_c = c; } private: int m_d; }; int main() { D d; return 0; }
2.虛繼承
- 為了解決多繼承時(shí)的命名沖突和冗余數(shù)據(jù)問(wèn)題,C++提出了虛繼承,使得在派生類中只保留一份間接基類的成員。在繼承方式前面加上virtual關(guān)鍵字就是虛繼承,請(qǐng)看下面的例子:
#include "iostream" using namespace std; // 間接基類 class A { protected: int m_a; }; // 直接基類B class B : virtual public A // 加上關(guān)鍵字virtual! { protected: int m_b; }; // 直接基類C class C : virtual public A { protected: int m_c; }; // 派生類D class D : public B, public C { public: void seta(int a) { m_a = a; } // 正確! void setb(int b) { m_b = b; } void setc(int c) { m_c = c; } private: int m_d; }; int main() { D d; return 0; } -
虛繼承的目的是讓某個(gè)類做出聲明,承諾愿意共享它的基類。其中,這個(gè)被共享的基類就稱為虛基類(Virtual Base Class),本例中的A就是一個(gè)虛基類。在這種機(jī)制下,不論虛基類在繼承體系中出現(xiàn)了多少次,在派生類中都只包含一份虛基類的成員。重新梳理一下本例的繼承關(guān)系,如下圖所示:
虛基類.jpg - 觀察這個(gè)新的繼承體系,我們會(huì)發(fā)現(xiàn)虛繼承的一個(gè)不太直觀的特征:必須在虛派生的真實(shí)需求出現(xiàn)前就已經(jīng)完成虛派生的操作。在上圖中,當(dāng)定義D類時(shí)才出現(xiàn)了對(duì)虛派生的需求,但是如果B類和C類不是從A類虛派生得到的,那么D類還是會(huì)保留A類的兩份成員。換個(gè)角度講,虛派生只影響從指定了虛基類的派生類中進(jìn)一步派生出來(lái)的類,它不會(huì)影響派生類本身。
3.虛基類成員的可見(jiàn)性
- 因?yàn)樵谔摾^承的最終派生類中只保留了一份虛基類的成員,所以該成員可以被直接訪問(wèn),不會(huì)產(chǎn)生二義性。此外,如果虛基類的成員只被一條派生路徑覆蓋,那么仍然可以直接訪問(wèn)這個(gè)被覆蓋的成員。但是如果該成員被兩條或多條路徑覆蓋了,那就不能直接訪問(wèn)了,此時(shí)必須指明該成員屬于哪個(gè)類。
- 以圖2中的菱形繼承為例,假設(shè)在A中定義了一個(gè)名為x的成員變量,當(dāng)我們?cè)贒中直接訪問(wèn)x時(shí),會(huì)有三種可能性:
- 如果B和C中都沒(méi)有x的定義,那么x將被解析為B的成員,此時(shí)不存在二義性。
- 如果B或C其中的一個(gè)類定義了x,也不會(huì)有二義性,派生類的x比虛基類的x優(yōu)先級(jí)更高。
- 如果B和C中都定義了x,那么直接訪問(wèn)x將產(chǎn)生二義性問(wèn)題。
4.虛繼承時(shí)的構(gòu)造函數(shù)
- 在虛繼承中,虛基類是由最終的派生類初始化的。換句話說(shuō),最終派生類的構(gòu)造函數(shù)必須要調(diào)用虛基類的構(gòu)造函數(shù)。對(duì)最終的派生類來(lái)說(shuō),虛基類是間接基類,而不是直接基類。這跟普通繼承不同,在普通繼承中,派生類構(gòu)造函數(shù)中只能調(diào)用直接基類的構(gòu)造函數(shù),不能調(diào)用間接基類的。
// 虛基類AA class AA{ protected: int m_a; public: AA(int a); }; // 類外定義虛基類AA的構(gòu)造函數(shù) AA::AA(int a):m_a(a){} // 直接派生類BB class BB: virtual public AA{ protected: int m_b; public: BB(int a, int b); void show(); }; BB::BB(int a, int b): AA(a), m_b(b){} void BB::show(){ cout << "m_a = " << m_a << ",m_b = " << m_b << endl; } // 直接派生類CC class CC: virtual public AA{ public: CC(int a, int c); void show(); protected: int m_c; }; CC::CC(int a, int c):AA(a), m_c(c){} void CC::show(){ cout << "m_a = " << m_a << ",m_c = " << m_c << endl; } // 間接派生類DD class DD: public BB, public CC{ protected: int m_d; public: DD(int a, int b, int c, int d); void show(); }; DD::DD(int a, int b, int c, int d):AA(a), BB(90, b), CC(100, c), m_d(d){} void DD::show(){ cout <<"m_a = " << m_a << ",m_b = " << m_b << ",m_c = " << m_c << ",m_d = " << m_d << endl; } int main() { BB bb(10, 20); bb.show(); CC cc(30, 40); cc.show(); DD dd(50, 60, 70, 80); dd.show(); return 0; } - 在最終派生類DD的構(gòu)造函數(shù)中,除了調(diào)用BB和CC的構(gòu)造函數(shù),還調(diào)用了AA的構(gòu)造函數(shù),這說(shuō)明DD不但要負(fù)責(zé)初始化直接基類BB和CC,還要負(fù)責(zé)初始化間接基類AA。而在以往的普通繼承中,派生類的構(gòu)造函數(shù)只負(fù)責(zé)初始化它的直接基類,再由直接基類的構(gòu)造函數(shù)初始化間接基類,用戶嘗試調(diào)用間接基類的構(gòu)造函數(shù)將導(dǎo)致錯(cuò)誤。
- 現(xiàn)在采用了虛繼承,虛基類AA在最終派生類DD中只保留了一份成員變量m_a,如果由BB和CC初始化m_a,那么BB和CC在調(diào)用AA的構(gòu)造函數(shù)時(shí)很有可能給出不同的實(shí)參,這個(gè)時(shí)候編譯器就會(huì)犯迷糊,不知道使用哪個(gè)實(shí)參初始化m_a。為了避免出現(xiàn)這種矛盾的情況,C++干脆規(guī)定必須由最終的派生類DD來(lái)初始化虛基類AA,直接派生類BB和CC對(duì)AA的構(gòu)造函數(shù)的調(diào)用是無(wú)效的。在代碼中,調(diào)用BB的構(gòu)造函數(shù)時(shí)試圖將m_a初始化為90,調(diào)用CC的構(gòu)造函數(shù)時(shí)試圖將m_a初始化為100,但是輸出結(jié)果有力地證明了這些都是無(wú)效的,m_a最終被初始化為50,這正是在DD中直接調(diào)用AA的構(gòu)造函數(shù)的結(jié)果。
- 另外,需要關(guān)注的是構(gòu)造函數(shù)的執(zhí)行順序。虛繼承時(shí)構(gòu)造函數(shù)的執(zhí)行順序與普通繼承時(shí)不同:在最終派生類的構(gòu)造函數(shù)調(diào)用列表中,不管各個(gè)構(gòu)造函數(shù)出現(xiàn)的順序如何,編譯器總是先調(diào)用虛基類的構(gòu)造函數(shù),再按照出現(xiàn)的順序調(diào)用其他的構(gòu)造函數(shù);而對(duì)于普通繼承,就是按照構(gòu)造函數(shù)出現(xiàn)的順序依次調(diào)用的。

