被遺棄的多重繼承(四十五)
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。
被遺棄的多重繼承(四十五)