程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++內存中的封裝、繼承、多態(下)

C++內存中的封裝、繼承、多態(下)

編輯:C++入門知識

上篇講述了內存中的封裝模型,下篇我們講述一下繼承和多態。

 

二、繼承與多態情況下的內存布局

由於繼承下的內存布局以及構造過程很多書籍都講得比較詳細,所以這裡不細講。重點講多態。

 

繼承有以下這幾種情況:

1.單一繼承

2.多重繼承

3.重復繼承

4.虛擬繼承

 

1.單一繼承的場合

假設有以下繼承關系,那麼大致的內存布局如下

 

代碼

 Child: GrandChild:

對象布局:

 

成員變量的布局很好理解,那麼在有虛函數的場合,虛函數表到底又是怎麼樣的呢?

為了解決這個問題我完善上面的代碼。

  fun1(){ cout<<<<  fun2(){ cout<<<<  fun3(){ cout<<<< Child:  fun1()  { cout<<<<  c_fun2(){ cout<<<<  c_fun3(){ cout<<<< GrandChild:  fun1()  { cout<<<<  c_fun2(){ cout<<<<  gc_fun3(){ cout<<<< 

 

我們先使用調試窗口查看一下虛函數表

可以看到三張表是不同的,可以看到fun1函數被改寫了兩次。

比較蛋疼的是grandc只能看到三個函數,君不見c_fun2和gc_fun3,還得自己動手來。

繼上篇的內容,我們使用pf這個函數指針:

(*PF)();
PF pf = NULL;

在主函數裡我們寫下代碼:

    * vtab = (*)*(*)& (; *vtab != NULL; vtab++= (PF)** member = (*)&<<*++member<<<<*++member<<<<*++member<<endl;

 

 

成員變量輸出結果與我們上篇的結論一致,咱們主要來看一下虛函數部分。

並且前三個函數同調試窗口的顯示結果。

 

我們依據以上結果可以得到這麼幾個結論:

1.單一繼承時,不同的類維護不同的虛函數表(only one),並且虛函數表初始情況是父類的樣子。

2.當發生overwrite時,例如fun1和c_fun2都會沖刷掉父類的虛函數,代替之。

3.沒有發生overwrite時,直接添加到虛函數表中。

 

圖示:

截止到這裡,結合上篇的內容,就能很容易理解為什麼使用父類指針能產生多態的效果了。

 

2.多重繼承的場合

假設有以下繼承關系,那麼大致的內存布局如下

由於是多繼承,根據1的觀點,單一繼承時一個類維護一個虛函數表。多繼承時怎麼辦呢?

那只能是繼承幾個類,就有幾張虛函數表了。

 

實例代碼如下:

  fun1(){ cout<<<<  fun2(){ cout<<<<  fun3(){ cout<<<<  fun1(){ cout<<<<  fun2(){ cout<<<<  fun3(){ cout<<<<  fun1(){ cout<<<<  fun2(){ cout<<<<  fun3(){ cout<<<< Derived: Base1,  Base2,   fun1(){ cout<<<<  d_fun(){ cout<<<<

 

通過調試窗口查看一下虛函數表:

可以明確的看到標注了for base,源自哪個基類的虛函數表。

並且可以看到fun1在三個表中全部被重寫了,那麼我們關心的d_fun到底會放在哪個表呢?

我們使用相同的辦法:

(*PF)();
PF pf = NULL;

    * vtab1 = (*)*(*)& (; *vtab1 != NULL; vtab1++= (PF)** member1 = (*)&<<*++member1<<
    * vtab2 = (*)*((*)&dd + (Base1)/ (; *vtab2 != NULL; vtab2++= (PF)** member2 = (*)((*)&dd + (Base1)/<<*++member2<<
    * vtab3 = (*)*((*)&dd + ((Base1)+(Base2))/ (; *vtab3 != NULL; vtab3++= (PF)** member3 = (*)((*)&dd + ((Base1)+(Base2))/<<*++member3<<endl;

 

偷了點懶,因為使用的是int型,所以沒有存在字節對齊的情況,直接使用的sizeof/4,使用這種偏移量來訪問不同的base區域。

以下是輸出結果:

我們可以看到d_fun被放到了第一個函數表中去了(聲明的次序的第一個,實例代碼是base1的部分)。

結論:

1.多重繼承的場合,overwirte時,父類的函數在三個表中會全部被重寫。

2.子類新添加的虛函數被放到第一個虛函數表中。

圖示:

 

3.重復繼承的場合

其實重復繼承只是多重繼承的特例,一切的規則依然按照多重繼承的規則實行。只是特殊在祖父類生成了兩個拷貝鏡像,形成數據重復,並且造成二義性。

無論從設計的的角度還是維護的角度,這都是一個失敗的選擇。

所以我們不重點討論,直接跳到虛擬繼承。

 

4.虛擬繼承的場合

關於虛擬繼承的對象模型,其實有多種方法,本文使用的的環境是vs2008,屬於微軟想的招兒。《深入C++對象模型》一書中明確指出了

虛擬繼承的場合,對象模型的構建方式沒有固定的標准,主要的思路是拆分成不變局部和共享局部。當然只有更好的方法,也都是為了達到更高的存取效率。

所以本文描述的內存布局或許只在微軟編譯器的場合成立,正因為如此,我們把重點放在虛擬繼承的要達到的效果上。

假設有以下繼承關系:

實例代碼:

  fun(){   cout<<<<  B_fun(){ cout<<<< Base1:   fun(){    cout<<<<  fun1(){   cout<<<<  B_fun1(){ cout<<<< Base2:   fun(){    cout<<<<  fun2(){   cout<<<<  B_fun2(){ cout<<<< Derived: Base1,   fun(){   cout<<<<  fun1(){  cout<<<<  fun2(){  cout<<<<  D_fun(){ cout<<<<

 

先來討論單一虛擬繼承的情況,看一下Base1的布局:

bb是Base的對象,bb1是Base1的對象。

明顯可以看到與普通單一繼承不同,使用了兩個虛函數指針,一個指向了虛基類Base的表,以及自己再生成一個表。

而指向虛基類Base的表的虛函數fun明顯被重寫了。

使用代碼讀取:

* vtab = (*)*(*)& (; *vtab != NULL; vtab++= (PF)*

 

 

這個循環運行會中斷,原因是vtab訪問了一個神奇的數字-4,這個是用來隔開的,不小心訪問了。(陳皓老師的一篇博文《C++對象的內存布局》也遇到了相同的問題,而GCC卻沒有)

足以證明,這裡的不變局部是Derived自己後來添加的函數。而共享局部fun跑到虛基類包含的虛函數表上去了。

我們使用二級指針來解決中斷的問題。

** pVtab = (**)&
    pf = (PF)pVtab[][
= (PF)pVtab[][
    cout << pVtab[][] << endl;
<< ()*((*)(&bb1)+) <<endl; 
<<<<(*)*((*)(&bb1)+) <<endl;

    
    pf = (PF)pVtab[][= (PF)pVtab[][<< pVtab[][] << endl;
<< ()*((*)(&bb1)+) <<endl; 

 

可以看出內存布局:

1.不變布局(子類)放在對象模型的前端,共享布局(虛基類)放在尾端。

2.其中子類部分,虛函數表使用了-4作為分隔結尾。接下來是子類成員變量值

3.虛基類屬於共享局部,是一個正常的虛函數表布局,並且重寫了fun函數。

圖示:

 

這樣是能夠保證共享部分處於虛基類中(包括虛函數表),不變部分處於子類中。

 

接下來看完整的繼承結構,解析Derived的布局。

使用代碼:

** pVtab = (**)&
    pf = (PF)pVtab[][= (PF)pVtab[][<< pVtab[][] << endl;
<< ()*((*)(&dd)+) <<endl; 

    
    pf = (PF)pVtab[][= (PF)pVtab[][<< pVtab[][] << endl;
    cout << ()*((*)(&dd)+) <<endl; 

    
    cout << ()*((*)(&dd)+) <<endl; 

    
    cout << <<(*)*((*)(&dd)+) <<= (PF)pVtab[][= (PF)pVtab[][<< ()*((*)(&dd)+) <<endl; 

 

運行結果:

與單一虛擬繼承類似:

1.按照聲明的次序,不變布局(父類)依次放在對象模型的前端,共享布局(虛基類)放在最尾端。

2.其中不變布局部分,虛函數表使用了-4作為分隔結尾。接下來是子類成員變量值

3.虛基類屬於共享局部,是一個正常的虛函數表布局,並且重寫了fun函數。

 

圖就不畫了,與單一虛擬繼承的情況類似。

引用《深入C++對象模型》一書的描述:

要在編譯器中支持虛擬繼承,困難度頗高。

難度在於,要找到一個足夠有效的辦法,將Base1和Base2各自維護的Base部分,折疊成為一個由Derived單一維護的Base部分,並且還可以保持base class和Derived class的指針之間的多態操作。

這也整是虛擬繼承要達到的效果。

 

至此,全篇差不多講完了。

主要參考書籍《深入C++對象模型》以及上文提到的陳皓老師的博文,內容稍長,難免有纰漏,望大家指正。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved