C++中的類繼承(4)繼承種類之單繼承&多繼承&菱形繼承

 一、單繼承與多繼承

  單繼承是通常的單一繼承,一個子類只 有一個直接父類時稱這個繼承關係爲單繼承。這種關係比較簡單是一對一的關係: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中存放了兩個虛基類指針,每一個虛基類表中存儲了偏移量。形象的內存佈局以下圖:

 

2.類中加數據成員

下面看一下擁有獨立數據成員的類的虛繼承,能夠更清晰的理解內存佈局:

 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 }

如今咱們直接看內存佈局:

 

相關文章
相關標籤/搜索