轉載:http://blog.csdn.net/pg_dog/article/details/70175488
今天呢,我們來講講菱形繼承與虛繼承。這兩者的講解是分不開的,要想深入了解菱形繼承,你是繞不開虛繼承這一點的。它倆有著什么關系呢?值得我們來剖析。?
菱形繼承也叫鉆石繼承,它是多繼承的一種特殊實例吧,它的基本架構如下圖:?
在我們的設想中,D所對應的對象模型應該如下圖所示:
?
下面我們來用一段代碼驗證一下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
上面顯示的大小似乎證實了我們的猜想,但實際上對象模型不是這樣的,如下圖所示?
?
但是你會發現,這里面存在一個問題,對象D中有兩個‘a’,存在數據冗余的問題,如果對象B,C中有兩個同名的函數或同名成員變量(本例中的變量‘a’),那么對象D在調用該函數或該成員變量時,該選擇調用哪個呢?這也就可以看出還存有二義性問題。那么該如何處理呢??
解決二義性問題很簡單,你在調用函數時加上作用域運算符(::),但是數據冗余問題還是沒有解決。那么編譯器是如何處理這兩個問題的呢??
為了解決二義性問題和數據冗余問題,C++引入了虛繼承這一概念。下面重點來看虛繼承。
虛繼承?
虛繼承又稱共享繼承,是面向對象編程的一種技術,是指一個指定的基類,在繼承體系結構中,將其成員數據實例共享給也從這個基類直接或間接派生的其他類。虛擬繼承是多重繼承中特有的概念,虛擬繼承就是為了解決多重繼承而出現的。?
這里我想引入《C++ Primer》這本書中對虛繼承的有關描述。
在C++語言中我們通過虛繼承的機制來解決共享問題。虛繼承的目的是令某個類作出聲明,承諾共享它的基類。其中,共享的基類子對象稱其為虛基類。在這種機制下,不論虛基類在繼承體系中出現了多少次,在派生類中都只含有唯一一個共享的虛基類子對象。
這里還有一個概念,虛基類。虛基類是通過virtual繼承而來的派生類的基類。例如:B虛繼承了A,所以A是B的虛基類,而不是說A是虛基類。?
看下圖了解普通基類與虛基類的區別:
按照上面的說法,在對象D中應該只含有一個共享的虛基類子對象,也就是例子中的_a。確實,這樣就解決了數據冗余與二義性問題。我們來驗證上面的的說法。(為了計算簡單,我將上例中每個類成員變量變為整形int)
下面我們來看一段代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
B和C都是虛擬繼承,?
按照我們之前的推理,對象D的結構應該如圖所示:?
?
我們來通過vs2013調試中的內存窗口來驗證一下:?
?
看到這個結果是不是嚇壞寶寶了?和我們預測的完全不一樣,對象A和B中的_a跑到了最底部,這種結構明顯沒有了數據冗余和二義性問題了,這是怎么實現的呢?這就要引入一新的概念——虛基類表。?
虛基類表:又稱虛基表,編譯器會給虛繼承而來的派生類生成一個指針vbptr指向一個虛基表,而虛基表中存放的是偏移量。?
我們來看對象D中的對象B,它的第一部分(第一行)就是虛基類表指針vbptr,它存的是虛基表的地址,虛基表中存的是共享基類成員變量_a的相對此位置的偏移量,我們來看看,“01259b60”是個地址,利用內存窗口我們可以發現里面存著兩部分第一行“00 00 00 00”和第二行“00 00 00 14”,虛基表中分兩部分:第一部分存儲的是對象相對于存放vptr指針的偏移量(在這就是“00 00 00 00”,偏移量為0),第二部分存儲的是對象中基類對象部分相對于存放vbptr指針的地址的偏移量(在這就是“00 00 00 14”),?
即20(十六進制下14就是十進制的20),也就是說偏移量是20個字節,你可以用他們的地址相減驗證一番。你可以看圖數一下,而對象D中的C的第一部分也是一樣,是個虛基表,存的一樣也是偏移量,它存的地址“00369b68”,里面是“00 00 00 0c”即十進制的12,即偏移量為12字節。可以看下圖:
下面我再講一個概念——虛函數,這會在下篇文章多態中重點講解,但是這里有必要了解一下。?
虛函數——類的成員函數前面加上virtual關鍵字,則這個函數被稱為虛函數。
虛函數:用于定義類型特定行為的成員函數。通過引用和指針對虛函數的調用直到運行時才被解析,依據是引用或指針所綁定對象的類型。(《C++ Primer》中定義)
虛函數重寫(覆蓋):當在子類定義了一個和父類完全相同的虛函數時,則稱這個這個子類的函數重寫了(覆蓋了)父類的虛函數。?
既然說到這,就有必要區分一下幾個概念:?
重載:在同一作用域內,函數名相同,參數不同,返回值可不同的一對函數被稱為重載。?
隱藏(重定義):在不同作用域(一般指基類和派生類),函數名相同,參數列表也相同,但不需要virtual關鍵字的一組函數稱為隱藏。?
覆蓋:不在同一作用域(一般指派生類和基類),完全相同(協變除外)基類中函數必須有virtual關鍵字的一對函數被稱為重定義。
注:?
1,基類中定義了虛函數,在派生類中該函數始終保持虛函數的特性。?
2,只有類的成員函數才能定義為虛函數。?
3,靜態成員函數不能定義為虛函數。?
4,如果在類外定義虛函數,只能在聲明處加virtual關鍵字,類外定義函數時不能加virtual關鍵字。?
5,構造函數不能為虛函數。?
6,最好不要將賦值運算符重載定義為虛函數,因為使用容易混淆。?
7,不要在構造函數和析構函數調用虛函數,在構造函數和析構函數中對象是不完整的,可能會發生未定義的行為。?
8,最好將基類的析構函數定義為虛函數。(注:雖然基類的析構函數和派生類的析構函數名稱不一樣,但構成覆蓋,因為編譯器做了特殊處理)?
9,虛繼承只對虛繼承子類后面派生出的子類有影響,對虛繼承自雷本身沒有影響。
純虛函數?
純虛函數——在成員函數的后面加上=0,則成員函數為純虛函數。一個純虛函數無需定義,但也可以定義,但是必須在類外,也就是說我們不能在類內部為一個帶有=0的函數提供函數體。包含純虛函數的類被稱為抽象類,也叫接口類。抽象類不能實例化出對象。他只是作為基類服務于派生類,如果派生類不對基類的虛函數進行覆蓋,那他仍將是抽象基類。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
繼承和友元?
友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員。
繼承和靜態成員?
基類中定義了靜態成員,則整個繼承體系中只有一個這樣的成員。無論派生出多少的子類,都只有一個靜態成員實例。