C++中的虛繼承與虛基類

技術(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)用的。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容