1. 程式人生 > >被遺棄的多重繼承(四十五)

被遺棄的多重繼承(四十五)

多重繼承 數據冗余 dynamic_cast 單繼承多接口 接口

我們在學習了 C++ 的繼承之後,有沒有想過一個類是否被允許繼承自多個父類呢?那麽在 C++ 中是支持編寫多重繼承的代碼的,即一個子類可以擁有多個父類。此時子類擁有所有父類的成員變量,子類繼承所有父類的成員函數,子類對象可以當作任意父類對象來使用。那麽多重繼承的語法如下所示,其本質與單繼承相同!

class Derived : public BaseA, public BaseB
{
    // ...
};

下來我們就以代碼為例來進行分析

#include <iostream>

using namespace std;

class BaseA
{
    int ma;
public:
    BaseA(int a)
    {
        ma = a;
    }
    
    int getA()
    {
        return ma;
    }
};

class BaseB
{
    int mb;
public:
    BaseB(int b)
    {
        mb = b;
    }
    
    int getB()
    {
        return mb;
    }
};

class Derived : public BaseA, public BaseB
{
    int mc;
public:
    Derived(int a, int b, int c) : BaseA(a), BaseB(b)
    {
        mc = c;
    }
    
    int getC()
    {
        return mc;
    }
    
    void print()
    {
        cout << "ma = " << getA() << ", "
             << "mb = " << getB() << ", "
             << "mc = " << mc << endl;
    }
};

int main()
{
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
    
    Derived d(1, 2, 3);
    
    d.print();
    
    cout << "d.getA() = " << d.getA() << endl;
    cout << "d.getB() = " << d.getB() << endl;
    cout << "d.getC() = " << d.getC() << endl;
    
    cout << endl;
    
    BaseA* pa = &d;
    BaseB* pb = &d;
    
    cout << "pa->getA() = " << pa->getA() << endl;
    cout << "pb->getB() = " << pb->getB() << endl;
    
    cout << endl;
    
    void* paa = pa;
    void* pbb = pb;
    
    if( paa == pbb )
    {
        cout << "Pointer to the same object!" << endl;
    }
    else
    {
        cout << "Error!" << endl;
    }
/*
    cout << "pa = " << pa << endl;
    cout << "pb = " << pb << endl;
    cout << "paa = " << paa << endl;
    cout << "pbb = " << pbb << endl;
*/    
    return 0;
}

我們在程序中定義了父類 A 和 B,子類 Derived 繼承自 A 和 B。我們先來打印下子類的內存大小,按照我們之前學習的知識可知,這肯定為 12。接著是定義了一個子類對象 d,通過調用它的 print 成員函數和繼承過來的 get* 函數打印值,看看初始化是否成功。接著是定義了兩個父類類型的指針並將他們指向子類對象 d,再用 void* 指針指向兩個父類指針,按理說它們都指向的是子類對象 d,所以在下面的判斷中,應該是相等的,打印的是 Pointer to the same object! 下來我們編譯看看結果

技術分享圖片

我們可以看到之前分析的都是對的,但是最後一個打印的竟然不是我們所期望的。也就是說,它們雖然指向的都是同一個對象的地址,但是地址竟然不相同。我們再來將註釋去掉,看看他們四個的指針究竟是多少?

技術分享圖片

我們看到他們打印的地址確實不一樣。這便是多重繼承帶來的問題之一了,通過多重繼承得到的對象可能擁有“不同的地址”!!!其關系圖如下

技術分享圖片

由上面的關系圖我們可以看出它們指向的地址確實是不一樣的,一個指向的是子類對象的頭部,另一個指向的是腰部,此問題無解

多重繼承的問題之二是可能會產生冗余的成員,如下圖

技術分享圖片

在上面的這幅圖中,Teacher 類和 Student 類繼承自 People 類,Doctor 類繼承自 Teacher 類 和 Student 類。就是一個在讀的博士原來是某學校的老師,但是他後來考上了在讀的博士,因此他也成了學生。所以他有多重身份,Teacher 會繼承 People 類的姓名和年齡,Student 也會繼承 People 類的姓名和年齡,這便造成了成員的冗余。下來我們以代碼為例來進行分析

#include <iostream>
#include <string>

using namespace std;

class People
{
    string m_name;
    int m_age;
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    
    void print()
    {
        cout << "name = " << m_name << ", "
             << "age = " << m_age << endl;
    }
};

class Teacher : public People
{
public:
    Teacher(string name, int age) : People(name, age)
    {
    }
};

class Student : public People
{
public:
    Student(string name, int age) : People(name, age)
    {
    }
};

class Doctor : public Teacher, public Student
{
public:
    Doctor(string name, int age) : Teacher(name, age), Student(name, age)
    {
    }
};

int main()
{
    Doctor d("zhang san", 22);
    
    d.print();
    
    return 0;
}

我們來編譯下看看

技術分享圖片

編譯的時候報錯了,它說不知道該調用哪個 print 函數。那麽我們在 main 函數中指定,分別來調用Teacher 和 Student 的 print 函數來看看

技術分享圖片

我們看到它打印了兩次,這邊造成了信息的冗余。當多重繼關系出現閉合時將產生數據冗余的問題!!!解決方案是采用虛繼承的方式。如下

技術分享圖片

解決數據冗余問題的方案便是虛繼承使得中間層不再關系頂層父類的初始化,最終子類必須直接調用頂層父類的構造函數。那麽這時問題就來了,當在進行架構設計中需要繼承時,便無法確定是使用直接繼承還是虛繼承?如果我們采用直接繼承而且是多重繼承的話,便會產生數據的冗余;如果是虛繼承的話,是可以解決數據冗余的問題,但是在經過了好幾次的繼承之後,我們還會那麽容易的找到頂層父類嗎?我們將上面的程序改為虛繼承,如下

#include <iostream>
#include <string>

using namespace std;

class People
{
    string m_name;
    int m_age;
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    
    void print()
    {
        cout << "name = " << m_name << ", "
             << "age = " << m_age << endl;
    }
};

class Teacher : virtual public People
{
public:
    Teacher(string name, int age) : People(name, age)
    {
    }
};

class Student : virtual public People
{
public:
    Student(string name, int age) : People(name, age)
    {
    }
};

class Doctor : public Teacher, public Student
{
public:
    Doctor(string name, int age) : People(name, age), Teacher(name, age), Student(name, age)
    {
    }
};

int main()
{
    Doctor d("zhang san", 22);
    
    d.print();
    
    return 0;
}

編譯看看結果

技術分享圖片

多重繼承的問題之三便是可能會產生多個虛函數表,如下

技術分享圖片

下來我們還是以代碼為例來進行分析

#include <iostream>
#include <string>

using namespace std;

class BaseA
{
public:
    virtual void funcA()
    {
        cout << "BaseA::funcA()" << endl;
    }
};

class BaseB
{
public:
    virtual void funcB()
    {
        cout << "BaseB::funcB()" << endl;
    }
};

class Derived : public BaseA, public BaseB
{

};

int main()
{
    Derived d;
    BaseA* pa = &d;
    BaseB* pb = &d;
    BaseB* pbe = (BaseB*)pa;
    
    
    cout << "sizeof(d) = " << sizeof(d) << endl;
    
    cout << "Using pa to call funcA()..." << endl;
    
    pa->funcA();
    
    cout << "Using pb to call funcB()..." << endl;
    
    pb->funcB();
    
    cout << "Using pbe to call funcB()..." << endl;
    
    pbe->funcB();

    return 0;
}

我們在程序的第 37 行打印對象 d 的內存大小,由於它虛繼承了兩個類,所以會產生兩個虛函數表指針,它的內存大小便會為 8。下來我們通過指針 pa 調用 funcA,很明顯它會打印出 BaseA::funcA(),而通過指針 pb 調用 funcB 便打印出 BaseB::funcB()。有意思的來了,我們之前在第 34 行用 BaseB 類型來強制轉換 BaseA 類型的指針 pa,我們通過它來打印下,看看會打印出什麽。我們期望的是打印 BaseB::funcB(),看看結果呢

技術分享圖片

我們看到前面打印的確實是如我們所分析的那樣,但是最後一個卻打印的是 funA 中的內容。我們很驚訝,我們在之前說過在 C++ 中要用新型的轉換關鍵字,繼承這便用的是 dynamic_cast,下來我們用它來進行轉換,再來打印這幾個指針的地址值。

技術分享圖片

我們看到打印的是我們所期望的內容。而且用強制類型轉換的指針 pbe 和用 dynamic_cast 關鍵字轉換的指針 pbc 打印的地址值是不一樣的。所以在需要進行強制類型轉換時,我們要使用新式類型轉換關鍵字。解決方案便是使用 dynamic_cast,如下

技術分享圖片

那麽多重繼承這麽多的問題,是不是就不用它了呢?不用的話,生活中的很多現象就用語言沒法描述了。因此,我們應該要正確的使用多重繼承,那麽在工程開發者的“多重繼承”方式什麽呢?單繼承某個類 + 實現(多個)接口。如下

技術分享圖片

在經過這麽多年的發展以後,前輩們便在實際工程中總結出了這些建議:a> 先繼承自一個類,然後實現多個接口;b> 父類提供 equal() 成員函數;c> equal() 成員函數用於判斷指針是否指向當前對象;d> 與多重繼承相關的強制類型轉換用 dynamic_cast 完成

下來我們還是以代碼為例進行分析

#include <iostream>
#include <string>

using namespace std;

class Base
{
protected:
    int mi;
public:
    Base(int i)
    {
        mi = i;
    }
    
    int getI()
    {
        return mi;
    }
    
    bool equal(Base* obj)
    {
        return (this == obj);
    }
};

class Interface1
{
public:
    virtual void add(int i) = 0;
    virtual void minus(int i) = 0;
};

class Interface2
{
public:
    virtual void multiply(int i) = 0;
    virtual void divide(int i) = 0;
};

class Derived : public Base, public Interface1, public Interface2
{
public:
    Derived(int i) : Base(i)
    {
    }
    
    void add(int i)
    {
        mi += i;
    }
    
    void minus(int i)
    {
        mi -= i;
    }
    
    void multiply(int i)
    {
        mi *= i;
    }
    
    void divide(int i)
    {
        if( i != 0 )
        {
            mi /= i;
        }
    }
};

int main()
{
    Derived d(100);
    Derived* p = &d;
    Interface1* pInt1 = &d;
    Interface2* pInt2 = &d;
    
    cout << "p->getI() = " << p->getI() << endl;    // 100
    
    pInt1->add(10);
    pInt2->divide(11);
    pInt1->minus(5);
    pInt2->multiply(8);
    
    cout << "p->getI() = " << p->getI() << endl;    // 40
    
    cout << endl;
    
    cout << "pInt1 == p : " << p->equal(dynamic_cast<Base*>(pInt1)) << endl;
    cout << "pInt2 == p : " << p->equal(dynamic_cast<Base*>(pInt2)) << endl;
    
    return 0;
}

我們定義了一個父類,定義了兩個接口。類 Derived 為多重繼承,初始化為 100,在第 79 行便會打印出 100,經過下面四步的操作之後,得到的結果應該是 40。第 90 和 91 行打印的應該都為 1,我們看看編譯結果

技術分享圖片

得到的結果和我們所分析的是一致的。通過對多重繼承的學習,總結如下:1、C++ 支持多重繼承的編程方式;2、多重繼承容易帶來的問題有可能出現“同一個對象的地址不同”的情況,虛繼承可以解決數據冗余的問題,虛繼承使得架構設計可能會出現問題;3、多繼承中可能出現多個虛函數表指針;4、與多重繼承相關的強制類型轉換用 dynamic_cast 完成;5、工程開發中采用“單繼承多接口”的方式使用多繼承;6、父類提供成員函數用於判斷指針是否指向當前對象


歡迎大家一起來學習 C++ 語言,可以加我QQ:243343083

被遺棄的多重繼承(四十五)