類存在虛繼承,虛函數的內存佔用問題

最近在準備找工作,複習的過程中,遇到了求解含有虛繼承、虛函數的類的內存大小計算問題(也就是sizeof的結果)。在這裏,做一些總結以便後來者更易理解。

1、我們知道,一個空類的sizeof值爲1.

2、加入一個虛函數後,其sizeof值爲4,是因爲對於類A,編譯器爲其建立了一個虛表,而A中保存了一份指向虛表的指針,指針就是一個地址,在32位(x86)下,地址的大小爲4個字節,所以sizeof值爲4。

3、當類B繼承A後,求其sizeof值也爲4,是因爲類B中保存了類A的一個副本,sizeof(B)就相當於它本身成員的大小再加上sizeof(A),而B中沒有成員,故值爲4。

4、但如果B中擁有自己的虛函數時,sizeof(B)的值依然爲4,是因爲編譯器並沒有爲B也生成一個虛表,而是將B的虛函數放在A的虛函數下面,所以B只要包含A的副本也就擁有了指向這個虛表的指針。

5、如果B虛繼承了A,這時候sizeof(B)卻變成了12,分析類的結構(VS中通過右鍵.cpp文件-->屬性-->左側的C/C++->命令行,然後在其他選項這裏寫上/d1 reportAllClassLayout  ,這時候再運行,將輸出中的顯示輸出來源改爲「生成」,就可以在輸出中看到所有類的結構)。

分析發現,編譯器爲B生成了屬於自己的虛表,賦予了B一個指向虛表的指針(上圖第一個 vfptr),然後又賦予B一個指向虛繼承表的指針(vbptr),緊接着是父類A的一個副本。也是就sizeof(B) = 一個指向B虛表的指針 + 一個指向虛繼承表的指針 + sizeof(A) = 12。當然如果B沒有自己的虛函數b(),也就不會有指向自己虛表的指針。

6、如果現在一個新的類C,虛繼承類B的話,我們可想而知sizeof(C) = 一個指向C虛表的指針 + 一個指向虛繼承表的指針 + sizeof(B) = 20。如果C沒有虛繼承B,也可以理解爲,C得到了B的虛表指針,而且會將自己的虛函數放在B的虛函數後面,所以sizeof(C)  = sizeof(B) 。

7、回到第5條,如果B中沒有自己的虛函數,而只是重寫了父類的虛函數。我們知道其sizeof(B) 應該爲8。這是因爲B中只含有一個指向虛繼承表的指針和父類A的副本。

如果類A擁有自己重載的構造函數呢?我們知道構造函數、析構函數並不影響類的大小,所以sizeof(A)的值並不會變,sizeof(B)也不會變。但是如果類B擁有了自己重載的構造函數呢?答案並不是你想象中的沒有變化,sizeof(A)結果不會變化,這點毫無疑問,但是sizeof(B)卻變成了12。

這是爲什麼?不是說好加入構造函數不影響類的大小麼?那麼我們分析一下這時候類B的結構。

在類B中,第一個vbptr是由於虛繼承,產生的一個指向父類A的指針,然後出現了一個指向擁有重寫虛函數的父類A的vtordisp指針,最後纔是父類A的副本,也就是vtordisp指針使得sizeof(B)變成了12。百度一下,找到了答案。

如果虛擬繼承中派生類重寫了基類的虛函數,並且在構造函數或者析構函數中使用指向基類的指針調用了該函數,編譯器會爲虛基類添加vtordisp域。雖然它並不一定被使用,但是我們必須將它添加以防用戶在構造函數或析構函數中調用虛函數。
我們知道,如果沒有給類寫構造函數,編譯器會自動給類一個默認的構造函數,而這個構造函數由於是自動生成的,那也就保證了它絕對不可能自行調用父類的虛函數。但是如果我們重載了構造函數,系統會給類添加上這個vtordisp域以防止我們調用。

這也就解釋了爲什麼sizeof(B)等於12的原因了。如果現在有個類C虛繼承了類B,然後重寫了從A繼承來的虛函數,且擁有了自己的構造函數,那麼sizeof(C) =  需要一個指向擁有重寫虛函數a()的父類A的vtordisp指針而B中已經含有這個vtordisp指針(0) + 一個指向虛繼承表的指針(4) + sizeof(B)  = 0 + 4 + 12 = 16。

那麼對於下面這種複雜的菱形繼承情況。容易計算,

sizeof(A) = 一個指向虛表的指針 = 4

sizeof(B) = 由於虛繼承且有自己的虛函數而產生的一個指向自己虛表的指針 + 由於虛繼承而產生的一個指向虛繼承表指針 + 由於自己擁有構造函數而產生的一個指向重寫虛函數的創造者A的vtordisp指針 + sizeof(A) = 16

sizeof(C) = 由於虛繼承而產生的一個指向虛繼承表指針 + 由於虛繼承且有自己的虛函數而產生的一個指向自己虛表的指針 + sizeof(A) = 12

最後我們分析sizeof(D) :

I.它繼承C,所以它的虛函數d()放在了C的虛表後面,從而沒有增加大小;

II.它繼承C的同時也得到了C的虛繼承表,並將自己虛繼承B的信息放在C的虛繼承表後,從而沒有增加大小;

III.它虛繼承了B,重寫了虛函數b()、a(),且擁有自己的構造函數,所以應該需要一個指向擁有重寫虛函數a()的父類A的vtordisp指針和一個指向擁有重寫虛函數b()的父類B的vtordisp指針,但由於B中已經有了這個指向擁有重寫虛函數a()的父類A的vtordisp指針,從而也沒有增加大小,但沒有指向擁有重寫虛函數b()的父類B的vtordisp指針,故這部分size相當於4。

IV.大家也知道菱形虛繼承所解決的問題就是D中含有重複祖父類A的副本,故計算size時需要減去一個sizeof(A)。

綜上sizeof(D) = 由於沒有虛繼承C而沒有增加新的虛表指針(0)+ 由於繼承C得到了C的虛繼承表而沒有增加新的虛繼承表指針(0)+由於B中含有指向擁有重寫虛函數a()的父類A的vtordisp指針所以不需要再添加(0)+ 由於D中含有構造函數且重寫了b()而需要指向擁有重寫虛函數b()的父類B的vtordisp指針(4)+ sizeof(B) + sizeof(C) - sizeof(A) = 0 + 0 + 0 + 4 + 12 + 16 - 4 = 28。

所以我們如果修改爲D也虛繼承C的話,sizeof(D) = 由於虛繼承B、C且自己擁有虛函數d()而增加的指向自己虛表的指針(4)+  由於虛繼承B、C從而需要重新創建自己的虛繼承表(4)+  由於B中含有指向擁有重寫虛函數a()的父類A的vtordisp指針所以不需要再添加(0)+ 由於C中不含有指向擁有重寫虛函數b()的父類B的vtordisp指針所以添加指向擁有重寫虛函數b()的父類B的vtordisp指針(4)+ sizeof(B) + sizeof(C) - sizeof(A)  -  由於C中沒有指向擁有重寫虛函數a()的父類A的vtordisp指針,所以不需要再減去相同的vtordisp指針(0)= 4 + 4 +0 + 4 +12 + 16 - 4  - 0 = 36。

至此,總結如下四條:

A. 虛繼承中,派生類需要添加一個指向虛繼承表的指針,而將相關繼承信息放到該表中。

B. 虛繼承中,如果派生類還有屬於自己的虛函數,則需要建立自己的虛表,並在類中添加指針指向該虛表。

C. 虛繼承中,如果派生類重寫了基類的虛函數,並擁有自己重載的構造函數或析構函數,則類會添加 被重寫的虛函數的構造類vtordisp域指針。而這種指針會在繼續被子類的虛繼承中生效,而不需要生成新的 vtordisp域指針

D. 在多繼承情況下,非虛繼承關係會使派生類獲得此父類的虛繼承表和虛表,從而將 與其他基類的虛繼承信息 和 虛函數 放在這個虛繼承表虛表 後,而不需要重新建立新的表,也就不需要添加新的指針。