天天看點

C++ 對象的記憶體布局(上)

簡而言之,我們一個類可能會有如下的影響因素:

1)成員變量

2)虛函數(産生虛函數表)

3)單一繼承(隻繼承于一個類)

4)多重繼承(繼承多個類)

5)重複繼承(繼承的多個父類中其父類有相同的超類)

6)虛拟繼承(使用virtual方式繼承,為了保證繼承後父類的記憶體布局隻會存在一份)

上述的東西通常是c++這門語言在語義方面對對象内部的影響因素,當然,還會有編譯器的影響(比如優化),還有位元組對齊的影響。在這裡我們都不讨論,我們隻讨論c++語言上的影響。

本篇文章着重讨論下述幾個情況下的c++對象的記憶體布局情況。

1)<b>單一的一般繼承</b>(帶成員變量、虛函數、虛函數覆寫)

2)<b>單一的虛拟繼承</b>(帶成員變量、虛函數、虛函數覆寫)

3)<b>多重繼承</b>(帶成員變量、虛函數、虛函數覆寫)

4)<b>重複多重繼承</b>(帶成員變量、虛函數、虛函數覆寫)

5)<b>鑽石型的虛拟多重繼承</b>(帶成員變量、虛函數、虛函數覆寫)

我們的目标就是,讓事情越來越複雜。

我們簡單地複習一下,我們可以通過對象的位址來取得虛函數表的位址,如:

我們同樣可以用這種方式來取得整個對象執行個體的記憶體布局。因為這些東西在記憶體中都是連續分布的,我們隻需要使用适當的位址偏移量,我們就可以獲得整個記憶體對象的布局。

本篇文章中的例程或記憶體布局主要使用如下編譯器和系統:

1)windows 7 和 vc++ 2010

2)cygwin 和 g++ 3.4.4

下面,我們假設有如下所示的一個繼承關系:

C++ 對象的記憶體布局(上)

請注意,在這個繼承關系中,父類,子類,孫子類都有自己的一個成員變量。而了類覆寫了父類的f()方法,孫子類覆寫了子類的g_child()及其超類的f()。

我們的源程式如下所示:

C++ 對象的記憶體布局(上)

使用圖檔表示如下:(紅色部分為本人修改)

C++ 對象的記憶體布局(上)

可見以下幾個方面:

1)虛函數表在最前面的位置。

2)成員變量根據其繼承和聲明順序依次放在後面。

3)在單一的繼承中,被overwrite的虛函數在虛函數表中得到了更新。

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。注意:子類隻overwrite了父類的f()函數,而還有一個是自己的函數(我們這樣做的目的是為了用g1()作為一個标記來标明子類的虛函數表)。而且每個類中都有一個自己的成員變量:

C++ 對象的記憶體布局(上)

我們的類繼承的源代碼如下所示:父類的成員初始為10,20,30,子類的為100

我們通過上面的程式來檢視子類執行個體的記憶體布局:上面程式中,注意我使用了一個s變量,其中用到了sizof(base)來找下一個類的偏移量。(因為我聲明的是int成員,是以是4個位元組,是以沒有對齊問題。關于記憶體的對齊問題,大家可以自行試驗,我在這裡就不多說了)

C++ 對象的記憶體布局(上)

使用圖檔表示是下面這個樣子:

C++ 對象的記憶體布局(上)

我們可以看到:

1) 每個父類都有自己的虛表。

2) 子類的成員函數被放到了第一個父類的表中。

3) 記憶體布局中,其父類布局依次按聲明順序排列。

4) 每個父類的虛表中的f()函數都被overwrite成了子類的f()。這樣做就是為了解決不同的父類類型的指針指向同一個子類執行個體,而能夠調用到實際的函數。

繼續閱讀