書上類繼承相關章節到這裏就結束了,這裏不妨說下C++內存分佈結構,咱們來看看編譯器是怎麼處理類成員內存分佈的,特別是在繼承、虛函數存在的狀況下。函數
工欲善其事,必先利其器,咱們先用好Visual Studio工具,像下面這樣一步一步來:工具
先選擇左側的C/C++->命令行,而後在其餘選項這裏寫上/d1 reportAllClassLayout,它能夠看到全部相關類的內存佈局,若是寫上/d1 reportSingleClassLayoutXXX(XXX爲類名),則只會打出指定類XXX的內存佈局。近期的VS版本都支持這樣配置。佈局
下面能夠定義一個類,像下面這樣:spa
1 class Base 2 { 3 int a; 4 int b; 5 public: 6 void CommonFunction(); 7 };
而後編譯一下,能夠看到輸出框裏面有這樣的排布:命令行
這裏不想花精力在內存對齊因素上,因此成員變量都設爲int型。3d
從這裏能夠看到普通類的排布方式,成員變量依據聲明的順序進行排列(類內偏移爲0開始),成員函數不佔內存空間。指針
再看下繼承,日後面添加以下代碼:code
1 class DerivedClass: public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 };
編譯,而後看到以下的內存分佈(父類的內存分佈不變,這裏只討論子類成員變量的內存分佈):對象
能夠看到子類繼承了父類的成員變量,在內存排布上,先是排布了父類的成員變量,接着排布子類的成員變量,一樣,成員函數不佔字節。blog
下面給基類加上虛函數,暫時註釋掉DerivedClass,看一下這時的內存排布:
1 class Base 2 { 3 int a; 4 int b; 5 public: 6 void CommonFunction(); 7 void virtual VirtualFunction(); 8 };
這個內存結構圖分紅了兩個部分,上面是內存分佈,下面是虛表,咱們逐個看。VS所帶編譯器是把虛表指針放在了內存的開始處(0地址偏移),而後再是成員變量;下面生成了虛表,緊跟在&Base1_meta後面的0表示,這張虛表對應的虛指針在內存中的分佈,下面列出了虛函數,左側的0是這個虛函數的序號,這裏只有一個虛函數,因此只有一項,若是有多個虛函數,會有序號爲1,爲2的虛函數列出來。
編譯器是在構造函數建立這個虛表指針以及虛表的。
那麼編譯器是如何利用虛表指針與虛表來實現多態的呢?是這樣的,當建立一個含有虛函數的父類的對象時,編譯器在對象構造時將虛表指針指向父類的虛函數;一樣,當建立子類的對象時,編譯器在構造函數裏將虛表指針(子類只有一個虛表指針,它來自父類)指向子類的虛表(這個虛表裏面的虛函數入口地址是子類的)。
因此,若是是調用Base *p = new Derived();生成的是子類的對象,在構造時,子類對象的虛指針指向的是子類的虛表,接着由Derived*到Base*的轉換並無改變虛表指針,因此這時候p->VirtualFunction,其實是p->vfptr->VirtualFunction,它在構造的時候就已經指向了子類的VirtualFunction,因此調用的是子類的虛函數,這就是多態了。
下面加上子類,並在子類中添加虛函數,像下面這樣:
1 class DerivedClass: public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction(); 7 };
能夠看到子類內存的排布以下:
上半部是內存分佈,能夠看到,虛表指針被繼承了,且仍位於內存排布的起始處,下面是父類的成員變量a和b,最後是子類的成員變量c,注意虛表指針只有一個,子類並無再生成虛表指針了;下半部的虛表狀況與父類是同樣的。
咱們把子類換個代碼,像這樣:
1 class DerivedClass1 : public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction2(); 7 };
注意到這時咱們並無覆寫父類的虛方法,而是重聲明瞭一個新的子類虛方法,內存分佈以下:
仍是隻有一個虛表指針,可是下方虛表的內容變化了,虛表的0號是父類的VirtualFunction,而1號放的是子類的VirtualFunction2。也就是說,若是定義了DerivedClass的對象,那麼在構造時,虛表指針就會指向這個虛表,之後若是調用的是VirtualFunction,那麼會從父類中尋找對應的虛函數,若是調用的是VirtualFunction2,那麼會從子類中尋找對應的虛函數。
咱們再改造一會兒類,像這樣:
1 class DerivedClass1 : public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction(); 7 void virtual VirtualFunction2(); 8 };
咱們既覆寫父類的虛函數,也有新添的虛函數,那麼能夠料想的到,是下面的這種內存分佈:
下面來討論多重繼承,代碼以下:
1 class Base 2 { 3 int a; 4 int b; 5 public: 6 void CommonFunction(); 7 void virtual VirtualFunction(); 8 }; 9 10 11 class DerivedClass1: public Base 12 { 13 int c; 14 public: 15 void DerivedCommonFunction(); 16 void virtual VirtualFunction(); 17 }; 18 19 class DerivedClass2 : public Base 20 { 21 int d; 22 public: 23 void DerivedCommonFunction(); 24 void virtual VirtualFunction(); 25 }; 26 27 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2 28 { 29 int e; 30 public: 31 void DerivedDerivedCommonFunction(); 32 void virtual VirtualFunction(); 33 };
內存分佈從父類到子類,依次以下:
Base中有一個虛表指針,地址偏移爲0
DerivedClass1繼承了Base,內存排布是先父類後子類。
DerivedClass2的狀況是相似於DerivedClass1的。
下面咱們重點看看這個類DerivedDerivedClass,由外向內看,它並列地排布着繼承而來的兩個父類DerivedClass1與DerivedClass2,還有自身的成員變量e。DerivedClass1包含了它的成員變量c,以及Base,Base有一個0地址偏移的虛表指針,而後是成員變量a和b;DerivedClass2的內存排布相似於DerivedClass1,注意到DerivedClass2裏面居然也有一份Base。
這裏有兩份虛表了,分別針對DerivedClass1與DerivedClass2,在&DerivedDericedClass_meta下方的數字是首地址偏移量,靠下面的虛表的那個-16表示指向這個虛表的虛指針的內存偏移,這正是DerivedClass2中的{vfptr}在DerivedDerivedClass的內存偏移。
若是採用虛繼承,像下面這樣:
1 class DerivedClass1: virtual public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction(); 7 }; 8 9 class DerivedClass2 : virtual public Base 10 { 11 int d; 12 public: 13 void DerivedCommonFunction(); 14 void virtual VirtualFunction(); 15 }; 16 17 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2 18 { 19 int e; 20 public: 21 void DerivedDerivedCommonFunction(); 22 void virtual VirtualFunction(); 23 };
Base類沒有變化,但往下看:
DerivedClass1就已經有變化了,原來是先排虛表指針與Base成員變量,vfptr位於0地址偏移處;但如今有兩個虛表指針了,一個是vbptr,另外一個是vfptr。vbptr是這個DerivedClass1對應的虛表指針,它指向DerivedClass1的虛表vbtable,另外一個vfptr是虛基類表對應的虛指針,它指向vftable。
下面列出了兩張虛表,第一張表是vbptr指向的表,8表示{vbptr}與{vfptr}的偏移;第二張表是vfptr指向的表,-8指明瞭這張表所對應的虛指針位於內存的偏移量。
DerivedClass2的內存分佈相似於DerivedClass1,一樣會有兩個虛指針,分別指向兩張虛表(第二張是虛基類表)。
下面來仔細看一下DerivedDerivedClass的內存分佈,這裏面有三個虛指針了,但base卻只有一份。第一張虛表是內含DerivedClass1的,20表示它的虛指針{vbptr}離虛基表指針{vfptr}的距離,第二張虛表是內含DerivedClass2的,12表示它的虛指針{vbptr}離虛基表指針{vfptr}的距離,最後一張表是虛基表,-20指明瞭它對應的虛指針{vfptr}在內存中的偏移。
虛繼承的做用是減小了對基類的重複,代價是增長了虛表指針的負擔(更多的虛表指針)。
下面總結一下(當基類有虛函數時):
1. 每一個類都有虛指針和虛表;
2. 若是不是虛繼承,那麼子類將父類的虛指針繼承下來,並指向自身的虛表(發生在對象構造時)。有多少個虛函數,虛表裏面的項就會有多少。多重繼承時,可能存在多個的基類虛表與虛指針;
3. 若是是虛繼承,那麼子類會有兩份虛指針,一份指向本身的虛表,另外一份指向虛基表,多重繼承時虛基表與虛基表指針有且只有一份。