C++繼承詳解三 ----菱形繼承、虛繼承

轉載:http://blog.csdn.net/pg_dog/article/details/70175488

今天呢,我們來講講菱形繼承虛繼承。這兩者的講解是分不開的,要想深入了解菱形繼承,你是繞不開虛繼承這一點的。它倆有著什么關系呢?值得我們來剖析。?
菱形繼承也叫鉆石繼承,它是多繼承的一種特殊實例吧,它的基本架構如下圖:?
這里寫圖片描述

在我們的設想中,D所對應的對象模型應該如下圖所示:

這里寫圖片描述?
下面我們來用一段代碼驗證一下:

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}char a;
};class B  :public A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}char b;
};class C :public A
{
public:C(){cout << "C()" << endl;}~C(){cout << "~C()" << endl;}int c;
};
class D :public B, public C
{
public:D(){cout << "D()" << endl;}~D(){cout << "~D()" << endl;}int d;};int main()
{cout << sizeof(A)<< endl;  //1cout << sizeof(B)<< endl;  //2cout << sizeof(C)<< endl;  //8cout << sizeof(D)<< endl;  //16system("pause");return 0;
}
  • 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)

下面我們來看一段代碼:

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}void print(){printf("A");}int _a;
};class B  :virtual public A   //B虛繼承A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}int _b;
};class C :virtual public A    //C虛繼承A
{
public:C(){cout << "C()" << endl;}~C(){cout << "~C()" << endl;}int _c;
};
class D :public B, public C
{
public:D(){cout << "D()" << endl;}~D(){cout << "~D()" << endl;}int _d;};int main()
{cout << sizeof(A)<< endl;cout << sizeof(B)<< endl;cout << sizeof(C)<< endl;cout << sizeof(D)<< endl;B bb;C cc;D dd;dd.B::_a = 1;dd.C::_a = 2;dd._b = 3;dd._c = 4;dd._d = 5;system("pause");return 0;
}
  • 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的函數提供函數體。包含純虛函數的類被稱為抽象類,也叫接口類。抽象類不能實例化出對象。他只是作為基類服務于派生類,如果派生類不對基類的虛函數進行覆蓋,那他仍將是抽象基類。

class Father    //抽象類(接口類)
{
public:virtual void fun() = 0;  //定義純虛函數
protected:int _a;
};class Child
{
public:virtual void fun() = 0; //覆蓋,否則Child也是抽象類(接口類)
}; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

繼承和友元?
友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員。

繼承和靜態成員?
基類中定義了靜態成員,則整個繼承體系中只有一個這樣的成員。無論派生出多少的子類,都只有一個靜態成員實例。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/384357.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/384357.shtml
英文地址,請注明出處:http://en.pswp.cn/news/384357.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux :IO多路復用模型

轉載&#xff1a;http://blog.csdn.net/mr253727942/article/details/50827127 一、IO多路復用定義 IO多路復用允許應用在多個文件描述符上阻塞&#xff0c;并在某一個可以讀寫時通知&#xff0c; 一般遵循下面的設計原則&#xff1a;、 IO多路復用&#xff1a;任何文件描述符…

leetcode(一)刷題兩數之和

給定一個整數數組 nums 和一個整數目標值 target&#xff0c;請你在該數組中找出 和為目標值 target 的那 兩個 整數&#xff0c;并返回它們的數組下標。 示例 1&#xff1a; 輸入&#xff1a;nums [2,7,11,15], target 9 輸出&#xff1a;[0,1] 解釋&#xff1a;因為 nums[…

Linux并發服務器編程之多線程并發服務器

轉載&#xff1a;http://blog.csdn.net/qq_29227939/article/details/53782198 上一篇文章使用fork函數實現了多進程并發服務器&#xff0c;但是也提到了一些問題&#xff1a; fork是昂貴的。fork時需要復制父進程的所有資源&#xff0c;包括內存映象、描述字等&#xff1b;目…

leetcode(二)二分法查找算法

給定一個 n 個元素有序的&#xff08;升序&#xff09;整型數組 nums 和一個目標值 target &#xff0c;寫一個函數搜索 nums 中的 target&#xff0c;如果目標值存在返回下標&#xff0c;否則返回 -1。 示例 1: 輸入: nums [-1,0,3,5,9,12], target 9 輸出: 4 解釋: 9 出現在…

leetcode(977)有序數組的平方

給你一個按 非遞減順序 排序的整數數組 nums&#xff0c;返回 每個數字的平方 組成的新數組&#xff0c;要求也按 非遞減順序 排序。 示例 1&#xff1a; 輸入&#xff1a;nums [-4,-1,0,3,10] 輸出&#xff1a;[0,1,9,16,100] 解釋&#xff1a;平方后&#xff0c;數組變為 […

IO多路復用之select全面總結(必看篇)

轉載&#xff1a;http://www.jb51.net/article/101057.htm 1、基本概念 IO多路復用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取&#xff0c;它就通知該進程。IO多路復用適用如下場合&#xff1a; &#xff08;1&#xff09;當客戶處理多個描述字時&#xff08;一般…

leetcode(189) 旋轉數組

**給定一個數組&#xff0c;將數組中的元素向右移動 k 個位置&#xff0c;其中 k 是非負數。 進階&#xff1a; 盡可能想出更多的解決方案&#xff0c;至少有三種不同的方法可以解決這個問題。 你可以使用空間復雜度為 O(1) 的 原地 算法解決這個問題嗎&#xff1f; 示例 1: …

I/O 多路復用之select

轉載&#xff1a;http://blog.csdn.net/u012432778/article/details/47347133 概述 Linux提供了三種 I/O 多路復用方案&#xff1a;select&#xff0c;poll和epoll。在這一篇博客里先討論select, poll 在將下一篇中介紹&#xff0c;epoll是Linux特有的高級解決方案&#xff0c;…

leetcode(283)移動零

283. 移動零 給定一個數組 nums&#xff0c;編寫一個函數將所有 0 移動到數組的末尾&#xff0c;同時保持非零元素的相對順序。 示例: 輸入: [0,1,0,3,12] 輸出: [1,3,12,0,0] 說明: 必須在原數組上操作&#xff0c;不能拷貝額外的數組。 盡量減少操作次數。 方法一&#xff1…

exec函數族實例解析

轉載&#xff1a;http://www.cnblogs.com/blankqdb/archive/2012/08/23/2652386.html fork()函數通過系統調用創建一個與原來進程(父進程)幾乎完全相同的進程(子進程是父進程的副本&#xff0c;它將獲得父進程數據空間、堆、棧等資源的副本。注意&#xff0c;子進程持有的是上述…

leetcode(167)兩數之和 II - 輸入有序數組

兩數之和 II - 輸入有序數組 給定一個已按照 升序排列 的整數數組 numbers &#xff0c;請你從數組中找出兩個數滿足相加之和等于目標數 target 。 函數應該以長度為 2 的整數數組的形式返回這兩個數的下標值。numbers 的下標 從 1 開始計數 &#xff0c;所以答案數組應當滿足 …

常量指針與指針常量的區別(轉帖)

轉載&#xff1a;http://www.cnblogs.com/witty/archive/2012/04/06/2435311.html 三個名詞雖然非常繞嘴&#xff0c;不過說的非常準確。用中國話的語義分析就可以很方便地把三個概念區分開。 一) 常量指針。 常量是形容詞&#xff0c;指針是名詞&#xff0c;以指針為中心的一個…

c/c++錯題總結

1.類名 對象名 默認調用“對象名()”這個構造函數&#xff0c;在棧內存中存在對象名&#xff0c;在堆內存中存在實際對象&#xff1b; 2.類名 對象名(一個或以上個參數) 默認調用相應的構造函數&#xff0c;在棧內存中存在對象名&#xff0c;在堆內存中也是存在實際對象的&a…

智能指針學習筆記

轉載&#xff1a;http://www.cnblogs.com/wuchanming/p/4411878.html 1. 介紹 本文介紹智能指針的使用。智能指針是c 中管理資源的一種方式&#xff0c;用智能指針管理資源&#xff0c;不必擔心資源泄露&#xff0c;將c 程序員 從指針和內存管理中解脫出來&#xff0c;再者&…

c++程序編譯過程

c程序編譯分成四個過程&#xff1a;編譯預處理&#xff0c;編譯&#xff0c;匯編&#xff0c;鏈接 編譯預處理&#xff1a;處理以#為開頭 編譯&#xff1a;將.cpp文件翻譯成.s匯編文件 匯編&#xff1a;將.s匯編文件翻譯成機器指令.o文件 鏈接&#xff1a;匯編生產的目標文件.o…

仿函數(函數對象)

轉載&#xff1a;http://www.cnblogs.com/wuchanming/p/4411867.html 本文乃作者學習《C標準程序庫》的學習筆記&#xff0c;首先介紹了仿函數&#xff08;函數對象&#xff09;和函數適配器&#xff08;配接器&#xff09;的概念&#xff0c;然后列出STL中所有的仿函數&#x…

C++ template —— 動多態與靜多態(六)

轉載&#xff1a;http://www.cnblogs.com/yyxt/p/5157517.html 前面的幾篇博文介紹了模板的基礎知識&#xff0c;并且也深入的講解了模板的特性。接下來的博文中&#xff0c;將會針對模板與設計進行相關的介紹。 ------------------------------------------------------------…

變量之間的區別

全局變量、局部變量、靜態全局變量、靜態局部變量的區別 c變量根據定義具有不同的生命周期&#xff0c;會有不同的作用域&#xff0c;主要有六個作用域&#xff1a;全局作用域&#xff0c;局部作用域&#xff0c;文件作用域&#xff0c;類作用域&#xff0c;語句作用域&#xf…

計算機的網絡體系以及參考模型

計算機的網絡體系以及參考模型一、OSI七層模型二、TCP/IP參考模型三、TCP/IP 五層參考模型四、OSI 模型和 TCP/IP 模型異同比較五、OSI 和 TCP/IP 協議之間的對應關系六、為什么 TCP/IP 去除了表示層和會話層&#xff1f;七、數據如何在各層之間傳輸&#xff08;數據的封裝過程…

C++ 模板詳解(二)

轉載&#xff1a;http://www.cnblogs.com/gw811/archive/2012/10/25/2736224.html 四、類模板的默認模板類型形參 1、可以為類模板的類型形參提供默認值&#xff0c;但不能為函數模板的類型形參提供默認值。函數模板和類模板都可以為模板的非類型形參提供默認值。 2、類模板的類…