[GeekBand][C++面向對象高級編程(下)]第四周作業

題目:分別給出下面的類型Fruit和Apple的類型大小(即對象size),并通過畫出二者對象模型圖以及你的測試來解釋該size的構成原因。

class Fruit{test
   int no;
   double weight;
   char key;
public:
   void print() {   }
   virtual void process(){   }
};
   
class Apple: public Fruit{
   int size;
   char type;
public:
   void save() {   }
   virtual void process(){   }
};

注意到:
析構函數設置為虛函數并不影響sizeof結果,只是在vtbl里面多增加一項。
觀察這道題目主要有兩點需要考慮。第一,當類里面存在虛函數時,這個類所占的內存就會比沒有虛函數時候大一點,多出來的原因是在類的成員變量前面會多出一個指針(vptr),它指向虛指針表(vtbl),虛指針表里面的每一個指針再指向對應的虛函數。從而實現動態綁定;第二,在C語言介紹struct時,就說過一點,大多數計算機,數據項要求從某個數量字節的倍數開始存放,如short從偶數地址開始,int則被對齊在4字節邊界。為了滿足內存對齊,在比較小的成員后面會加入補位。在用不同的操作系統和編譯器時也發現了,sizeof的結果有所不同,所以這道題目并沒有正確的答案,本次實驗是在macOS Sierra Version 10.12.5 64-bit操作系統下,用Apple LLVM version 8.1.0 (clang-802.0.38)變異的,GCC的版本是4.2.1。

  1. 對象模型圖:

在這篇博客中借用一下這張圖。
[Boolan] C++第四周 homework 虛函數表與內存對齊

modelC.png

大概就是這個意思,虛函數表的指針(vptr)在內存中會出現在其他所有成員之前,C++語言規范明確定義了內存上的成員變量的順序和代碼定義時的順序是一致的(為了保證與C語言兼容)。正因為存在這樣的順序,所以在初始化子類的時候,會先初始化父類的成員變量,再初始化子類的。對象切割(Object Slicing)也可以順利進行。

vptr的位置在規范中沒有確定。當然我們可以去測試一下看看vptr到底在什么位置。測試代碼如下:

    Fruit f1, f2;
    Apple a1;
    int* pf1 = (int*) &f1;
    int* pf2 = (int*) &f2;
    int* hpf1 = (int*) *pf1;
    int* hpf2 = (int*) *pf2;
    cout << *pf1 << endl << *(pf1 + 1) << endl;
    cout << *pf2 << endl << *(pf2 + 1) << endl;
    cout << hpf1 << endl;
    int* pa1 = (int*) &a1;
    int* hpa1 = (int*) *pa1;
    cout << hpa1 << endl;
    return 0;

某一次出來的結果是:

94818464
1
94818464
1
0x5a6d0a0
0x5a6d0c8
  1. Sizeof和解析
    在之前聲明的環境下,編譯得到的結果如下:
sizeof(Fruit) = 32
sizeof(Apple) = 40
size of Fruit.no (int): 4
size of Fruit.weight(double):8
size of Fruit.key(char): 1
size of Apple.size (int): 4
size of Apple.tpye(char):1
Fruit        = 5791d410
Fruit.no     = 5791d418
Fruit.weight = 5791d420
Fruit.key    = 5791d428
Apple        = 5791d3e8
Apple.no     = 5791d3f0
Apple.weight = 5791d3f8
Apple.key    = 5791d400
Apple.size   = 5791d404
Apple.type   = 5791d408

對應的內存圖粗粗弄了一下,應該是這樣:

size.png

Apple是Fruit的子類,此為兩級的單鏈繼承結構。在Apple和Fruit對象內部,均遵循以下原則:

  1. 對象中的第一個成員是指向虛表的虛指針;
  2. 對象是按照聲明中的順序被保存的;

對于編譯器而言,其遵循以下的原則:

  1. 按聲明中出現的順序進行內存分配
  2. 要求數據成員的起始地址也必須是內部最大基本數據類型的整數倍,也就是說,在虛指針和數據成員之間必須存在4個占位字節
  3. 如果類中存在虛函數,在對象的起始處會有虛指針。
  4. 在所有變量的內存分配結束后,對象要填補成內存中的最大的基本類型變量的倍數。例如,如果一個類中最大的基本類型是double,那么它最后需要填補成8的整數倍。

還有三個特點在Fruit和Apple的關系中沒有涉及到,他們是:

  1. 多重繼承的情況下,在每個基類的前邊上會有不同的vptr;
  2. 如果在派生類中存在新的虛函數,則會產生一個兼容基類的虛表,而不會添加新的表;
  3. 組合關系時,內部類的起始地址應從內部類的最大的基本數據類型的整數倍處開始。
    綜合前4個特點,可以計算得到Fruit的大小是((4+4)+(4+4)+8+(1+7))=32Bytes;而Apple的大小是(32+4+(1+4))=40Bytes。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容