單繼承是通常的單一繼承,一個子類只 有一個直接父類時稱這個繼承關係爲單繼承。這種關係比較簡單是一對一的關係:ios
多繼承是指 一個子類有兩個或以上直接父類時稱這個繼承關係爲多繼承。這種繼承方式使一個子類能夠繼承多個父類的特性。多繼承能夠看做是單繼承的擴展。派生類具備多個基類,派生類與每一個基類之間的關係仍可看做是一個單繼承。多繼承下派生類的構造函數與單繼承下派生類構造函數類似,它必須同時負責該派生類全部基類構造函數的調用。同時,派生類的參數個數必須包含完成全部基類初始化所需的參數個數。在子類的內存中它們是按照聲明定義的順序存放的,下面的截圖將清晰看到。
編程
菱形繼承也叫鑽石繼承函數
可是多繼承存在一個問題,要想研究這個問題,咱們先從單繼承講起。來看內存空間:佈局
1 class Base 2 { 3 public: 4 Base() { 5 cout << "B()" << endl; 6 } 7 int b1; 8 }; 9 class Derive : public Base 10 { 11 public: 12 Derive() { 13 cout << "D()" << endl; 14 } 15 int d1; 16 }; 17 int main() 18 { 19 Test(); 20 getchar(); 21 return 0; 22 }
多繼承的內存空間:spa
1 class Base 2 { 3 public: 4 Base() { 5 cout << "B()" << endl; 6 } 7 int b1; 8 }; 9 class C 10 { 11 public: 12 C() { 13 cout << "C()" << endl; 14 } 15 int c; 16 }; 17 class Derive : public Base, public C 18 { 19 public: 20 Derive() { 21 cout << "D()" << endl; 22 } 23 int d1; 24 };
菱形繼承內存中數據分佈:設計
1 class A 2 { 3 public: 4 A() { 5 cout << "A()" << endl; 6 } 7 int a; 8 }; 9 class Base:public A 10 { 11 public: 12 Base() { 13 cout << "B()" << endl; 14 } 15 int b1; 16 }; 17 class C: public A 18 { 19 public: 20 C() { 21 cout << "C()" << endl; 22 } 23 int c; 24 }; 25 class Derive : public Base, public C 26 { 27 public: 28 Derive() { 29 cout << "D()" << endl; 30 } 31 int d1; 32 };
在A類中初始化int a=4則可清楚的看到菱形繼承中內存分佈3d
因此子類Derive中有兩份A類中的數據成員,這形成了訪問二義性和數據冗餘的問題指針
這就是我前面說的多繼承存在的問題。能夠這樣訪問code
1 tmp.C::a=4; 2 tmp.Base::a=5;
有兩個概念能夠解釋C++對象模型:cdn
一、語言中直接支持面向對象程序設計的部分。
二、對於各類支持的底層實現機制。
菱形繼承對象模型以下:
還有另一個方法解決這個問題,咱們要用到一種新的繼承方法:虛繼承 是面向對象編程中的一種技術,是指一個指定的基類,在繼承體系結構中,將其成員數據實例共享給也從這個基類型直接或間接派生的其它類,它可共享的特性,避免了拷貝多份相同的數據,從而解決菱形繼承的二義性和數據冗餘的問題。看下面這段代碼:
1 class Base 2 { 3 public: 4 Base() { 5 cout << "B()" << endl; 6 } 7 int b1; 8 }; 9 class Derive : virtual public Base 10 { 11 public: 12 Derive() { 13 cout << "D()" << endl; 14 } 15 int d1; 16 }; 17 void Test() 18 { 19 Derive tmp; 20 tmp.d1 = 1; 21 tmp.b1 = 2; 23 } 24 int main() 25 { 26 Test(); 27 getchar(); 28 return 0; 29 }
虛擬繼承的關鍵字---virtual
下圖爲單繼承的內存分佈:
圖中的偏移量地址其實爲一個指向基類偏移量表的指針。
虛擬繼承是雖然不是多重繼承中特有的概念。但虛擬基類是爲解決多重繼承而出現的。
下圖能夠看出虛基類和非虛基類在多重繼承中的區別
虛繼承的提出就是爲了解決多重繼承時可能會保存兩份副本的問題,也就是說用了虛繼承就只保留了一份副本,可是這個副本是被多重繼承的基類所共享的,該怎麼實現這個機制呢?待我慢慢道來
1.類中無其它數據成員時
1 class B //基類 2 { 3 public: 4 B() 5 { 6 cout << "B" << endl; 7 } 8 ~B() 9 { 10 cout << "~B()" << endl; 11 } 12 }; 13 class C1 :virtual public B 14 { 15 public: 16 C1() 17 { 18 cout << "C1()" << endl; 19 } 20 ~C1() 21 { 22 cout << "~C1()" << endl; 23 } 24 }; 25 class C2 :virtual public B 26 { 27 public: 28 C2() 29 { 30 cout << "C2()" << endl; 31 } 32 ~C2() 33 { 34 cout << "~C2()" << endl; 35 } 36 }; 37 class D :public C1, public C2 38 { 39 public: 40 D() 41 { 42 cout << "D()" << endl; 43 } 44 ~D() 45 { 46 cout << "~D()" << endl; 47 } 48 }; 49 50 int main() 51 { 52 cout << sizeof(B) << endl; 53 cout << sizeof(C1) << endl; 54 cout << sizeof(C2) << endl; 55 cout << sizeof(D) << endl; 56 return 0; 57 }
輸出結果爲:
結果分析:首先,基類中除了構造函數和析構函數沒有其餘成員了,因此 sizeof(B) = 1;這裏再提一個問題
有的初學者可能會問爲何爲1呢?首先類在內存中的存儲是這樣的:
若是有一個類Base定義以下例:
1 class Base 2 { 3 public: 4 void fun(); 5 int b; 6 }; 7 int Test() 8 { 9 Base b1,b2,b3; 10 }
那麼在內存中的對象模型以下圖:
成員函數是單獨存儲的,而且全部爲類對象公用。
類的實例化要求每一個實例對象在有獨立無二的地址空間,而空類也能夠實例化。編譯器要區分開全部的類對象,就要給對象一個地址,只是一個佔位符,表示這個對象存在,而且讓編譯器給這個對象分配地址。至於佔多少位,由編譯器決定,這裏空類的大小爲1,是在VS2015中,其餘編譯器可能不一樣。
因爲C1與C2都是虛擬繼承,故會在C1,C2內存起始處存放一個vbptr,爲指向偏移量表的指針。因此C1和C2大小爲4,這就是指針的大小了。D的大小就是繼承的兩個指針的大小了。這裏再詳細解釋一下偏移量表,是什麼的偏移量吶?
咱們在main函數中生成一個C1類對象c1:
1 int main() 2 { 3 C1 c1; 4 return 0; 5 }
內存佈局以下:
由圖能夠看出,c1佔了四個字節,存了一個指針變量,指針變量的內容就是 c1 的 vbptr 指向的偏移量表的地址。偏移量表有八個字節,分別存的爲0和4。 那麼0和4表明的都是什麼呢? 虛基類表存放的爲兩個偏移地址,分別爲0和4。其中0表示c1對象地址相對於存放vbptr指針的地址的偏移量。(由於vbptr指針是屬於c1對象的,c1對象地址相對於vbptr指針的地址偏移量爲0。這裏我把它這個表叫作偏移量表,避免與後面多態中的虛表混淆。)
而4表示c1對象中基類對象部分相對於存放vbptr指針的地址的偏移量,能夠用 &c1(B)-&vbpt 表示,其中&c1(B)表示對象c1中基類B部分的地址。
c2的內存佈局與c1同樣,由於C1,C2都是虛繼承自B基類,且C1,C2都沒有獨自的數據成員。
總結:C1,C2是虛繼承自基類B,因此編譯器會給C1,C2中生成一個指針vbptr指向一個偏移量表,即指針vbptr的值是偏移量表的地址。表中存放對象相對於偏移量表指針的偏移量。表中分兩部分,第一部分存儲的是對象相對於存放vptr指針的偏移量,能夠用&(對象名)->vbptr_(對象名)來表示。對c1對象來講,能夠用&c1->vbprt_c1來表示。表的第二部分存儲的是對象中基類對象部分相對於存放vbptr指針的地址的偏移量,咱們知道在本例中基類對象與指針偏移量就是指針的大小。
下面再來看看D的內存結構:
如圖所示,d中存放了兩個虛基類指針,每一個虛基類表中存儲了偏移量。形象的內存佈局以下圖:
下面看一下擁有獨立數據成員的類的虛繼承,能夠更清晰的理解內存佈局:
1 #include <iostream> 2 using namespace std; 3 4 class B 5 { 6 public: 7 B() 8 { 9 cout << "B" << endl; 10 } 11 ~B() 12 { 13 cout << "~B()" << endl; 14 } 15 int b; 16 }; 17 class C1 :virtual public B 18 { 19 public: 20 C1() 21 { 22 cout << "C1()" << endl; 23 } 24 ~C1() 25 { 26 cout << "~C1()" << endl; 27 } 28 int c1; 29 }; 30 class C2 :virtual public B 31 { 32 public: 33 C2() 34 { 35 cout << "C2()" << endl; 36 } 37 ~C2() 38 { 39 cout << "~C2()" << endl; 40 } 41 int c2; 42 }; 43 class D :public C1, public C2 44 { 45 public: 46 D() 47 { 48 cout << "D()" << endl; 49 } 50 ~D() 51 { 52 cout << "~D()" << endl; 53 } 54 void fun() 55 { 56 b = 0; 57 c1 = 1; 58 c2 = 2; 59 d = 3; 60 } 61 int d; 62 }; 63 64 int main() 65 { 66 cout << sizeof(B) << endl; 67 cout << sizeof(C1) << endl; 68 cout << sizeof(C2) << endl; 69 cout << sizeof(D) << endl; 70 D d; 71 d.fun(); 72 return 0; 73 }
輸出結果爲:
B佔四個字節沒有問題,由於B類中有int b數據成員,因此B類佔四個字節。 C1,C2是虛繼承自B類的,因此C1,C2的內存佈局是類似的,在這裏我只分析一下C1。 我在C1類中加一個Fun成員函數,爲了更清楚的看到內存佈局:
1 class C1 :virtual public B 2 { 3 public: 4 C1() 5 { 6 cout << "C1()" << endl; 7 } 8 ~C1() 9 { 10 cout << "~C1()" << endl; 11 } 12 void Fun() 13 { 14 b = 5; 15 c1 = 6; 16 } 17 int c1; 18 }; 19 int main() 20 { 21 C1 c1; 22 c1.Fun(); 23 return 0; 24 }
在main函數中生成對象c1,C1=int+int+指針=4+4+4=12,再來看一看內存佈局:
如今來看看D類的內存佈局:
1 class D :public C1, public C2 2 { 3 public: 4 D() 5 { 6 cout << "D()" << endl; 7 } 8 ~D() 9 { 10 cout << "~D()" << endl; 11 } 12 void fun()//fun()函數主要幫助咱們看D類的內存佈局 13 { 14 b = 0;//基類數據成員 15 c1 = 1;//C1類數據成員 16 c2 = 2;//C2類數據成員 17 d = 3;//D類本身的數據成員 18 } 19 int d; 20 };
內存佈局以下:
下面再看看多重虛擬繼承
1 class A 2 { 3 public: 4 A() { 5 cout << "A()" << endl; 6 } 7 int a ; 8 }; 9 class Base : virtual public A 10 { 11 public: 12 Base() { 13 cout << "B()" << endl; 14 } 15 int b1; 16 }; 17 class C:virtual public A 18 { 19 public: 20 C() { 21 cout << "C()" << endl; 22 } 23 int c; 24 }; 25 class Derive : virtual public Base, virtual public C 26 { 27 public: 28 Derive() { 29 cout << "D()" << endl; 30 } 31 int d1; 32 }; 33 void Test() 34 { 35 Derive tmp; 36 tmp.d1 = 1; 37 tmp.b1 = 2; 38 tmp.c = 3; 39 tmp.a = 4; 40 } 41 int main() 42 { 43 Test(); 44 getchar(); 45 return 0;
46 }
如今咱們直接看內存佈局: