天天看點

C++:94---類繼承(菱形繼承、虛繼承(virtual虛基類))

一、菱形繼承

  • 在介紹虛繼承之前介紹一下菱形繼承
  • 概念:A作為基類,B和C都繼承與A。最後一個類D又繼承于B和C,這樣形式的繼承稱為菱形繼承
  • 菱形繼承的缺點:
  • 資料備援:在D中會儲存兩份A的内容
  • 通路不明确(二義性):因為D不知道是以B為中介去通路A還是以C為中介去通路A,是以在通路某些成員的時候會發生二義性
  • 缺點的解決:
  • 資料備援:通過下面“虛繼承”技術來解決(見下)
  • 通路不明确(二義性):通過作用域通路符::來明确調用。虛繼承也可以解決這個問題
C++:94---類繼承(菱形繼承、虛繼承(virtual虛基類))

示範案例

class A
{
public:
    A(int a) :m_a(a) {}
    int getMa() { return m_a; }
private:
    int m_a;
};

class B :public A
{
public:
    B(int a, int b) :A(a), m_b(b) {}
private:
    int m_b;
};

class C :public A
{
public:
    C(int a, int c) :A(a), m_c(c) {}
private:
    int m_c;
};

class D :public B, public C
{
public:
    D(int a, int b, int c, int d) :B(a, b), C(a, c), m_d(d) {}
    void func()
    {
        /*錯誤,通路不明确
        std::cout << getMa();*/

        //正确,通過B通路getMa()
        std::cout << B::getMa();
    }
private:
    int m_d;
};      

二、虛繼承

  • 虛繼承的作用:為了保證公共繼承對象在建立時隻儲存一分執行個體
  • 虛繼承解決了菱形繼承的兩個問題:
  • 資料備援:頂級基類在整個體系中隻儲存了一份執行個體
  • 通路不明确(二義性):可以不通過作用域通路符::來調用(原理就是因為頂級基類在整個體系中隻儲存了一份執行個體)
  • 共享的基類對象成為“虛基類”
  • 說明:虛繼承不會影響派生類本身,隻是對虛基類進行的說明
  • 通過在繼承清單中使用virtual關鍵字來說明,virtual與繼承說明符(public、protected、private)的位置可以互換

示範案例

  • 下面的ZooAnimal是一個虛基類,Bear和Raccoon分别虛繼承于ZooAnimal
C++:94---類繼承(菱形繼承、虛繼承(virtual虛基類))
class ZooAnimal {}; //虛基類

class Bear :public virtual ZooAnimal {};    //虛繼承
class Raccoon :public virtual ZooAnimal {}; //虛繼承

//Panda隻儲存一份ZooAnimal的定義
class Panda :public Bear, public Raccoon, public Endangered {};      

三、虛繼承中的類型轉換

  • 虛繼承中也可以将派生類抓換為基類,用基類的指針/引用指向于派生類

菱形繼承中的類型轉換

  • 菱形繼承中會發生錯誤,不能将派生類轉換為基類
  • 原理是差不多的,就是因為派生類中擁有多份基類的實體,是以不能轉換,會産生二義性
class A {};

class B: publi A {};

class C: publi A {};

class D: public B, public C {};

int main()
{
    D d;
    A* pa = &d; //錯誤

    return 0;
}      

虛繼承中的類型轉換

class ZooAnimal {};

class Bear :public virtual ZooAnimal {};
class Raccoon :public virtual ZooAnimal {};

class Panda :public Bear, public Raccoon, public Endangered {};

void dance(const Bear&);
void rummage(const Raccoon&);
ostream& operator<<(ostream&, const ZooAnimal&);

int main()
{
    Panda ying_yang;

    dance(ying_yang);  //正确,把一個Panda對象當成Bear傳遞
    rummage(ying_yang);//正确,把一個Panda對象當成Raccoon 傳遞
    cout << ying_yang; //正确,把一個Panda對象當成ZooAnimal傳遞

    return 0;
}      

四、虛基類成員的可見性與隐藏

  • 規則如下:
  • 虛基類的成員沒有被任何派生類隐藏,那麼該成員可以直接通路,并且不會産生二義性
  • 如果虛基類的成員隻被一條派生路徑隐藏,則我們仍然可以直接通路這個被隐藏的版本
  • 如果虛基類的成員多多個派生路徑隐藏,則會産生二義性
  • 例如,D1和D2虛繼承與B,D繼承于D1和D2,并且B有一個x成員:
  • 如果D1和D2都沒有x的定義:此時對x的通路不會産生二義性,因為隻含有x的一個執行個體
  • 如果D1中有x的定義而D2沒有:同樣沒有二義性,派生類的x比虛基類B的x優先級更高(或者D1中沒有x的定義而D2有x的定義)
  • 如果D1和D2都有x的定義:對x的通路會産生二義性
C++:94---類繼承(菱形繼承、虛繼承(virtual虛基類))
  • 解決二義性最好的辦法就是在派生類為成員自定義新的執行個體

五、虛繼承的構造函數

  • 虛繼承中的構造函數與普通繼承的構造函數不一樣:
  • 普通繼承:派生類可以不為間接基類(基類的基類)進行構造函數的調用
  • 虛繼承:不論派生類屬于哪一層,派生類都需要對虛基類進行構造
  • 原因:假設以下間接派生類沒有為虛基類進行構造,那麼當間接派生類進行構造時,會對虛基類進行重複的構造函數的調用(例如下面的示範案例D如果不顯式構造A,那麼當構造B和C的時候,B和C都會構造一次A,進而造成錯誤)。是以我們需要在間接派生類中為虛基類進行構造,進而避免了重複構造的二義性

示範案例

//普通繼承
class A {
public:
    A(int a);
};

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

class C :public B {
public:
    C() :B(10) {} //可以不為A進行構造,因為A的構造已經交給B了
};      
//虛繼承
class A {
public:
    A(int a);
};

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

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

class D :public B,public C {
public:
    //D() :B(10), C(20) {} 錯誤的,必須顯式為A進行構造
    D() :A(5), B(10), C(20) {} //正确
};      

構造函數的執行順序

  • 規則:虛基類總是先于非虛基類構造,與它們在繼承體系中的次序和位置無關
  • 例如,在上面的示範案例中,構造順序為:A-->B-->C-->D
  • 下面再示範一個有多個虛基類的例子,其構造函數執行熟悉怒為:
  • ZooAnimal
  • ToyAnimal
  • Character
  • BookCharacter
  • Bear
  • TeddyBear
C++:94---類繼承(菱形繼承、虛繼承(virtual虛基類))
class Character {};
class BookCharacter :public Character {};

class ZooAnimal {};
class Bear :public virtual ZooAnimal {};

class ToyAnimal {};

class ReddyBear :public BookCharacter, public Bear, public virtual ToyAnimal {};      

析構函數

  • 析構函數的執行順序與構造函數執行順序相反