對c++記憶體布局有過一定了解,但是一直都很曖昧。為了搞清楚,決定**一下c++記憶體布局。
本文在vc2008下實驗,有興趣的同學可以在g++實驗下
經過仔細研究。現在總結出來3條規則:
規則1.乙個沒有繼承的c++物件的布局總是(虛表指標 + 成員變數),虛表指標至少為4位元組,如果你的成員有double的話,由於記憶體對齊的原因,虛表指標為8位元組。如果沒有虛函式,那麼虛表指標可以省去。
規則2.如果乙個類derived從classb1, classb2, ....classbn類多繼承(非虛繼承),那麼該類物件的記憶體結構生成過程(這裡是指猜測的編譯時的生成過程而非執行時的過程)是:
a. 把classb1到classbn的記憶體布局全部按**順序依次搬過來拼接起。
b. 把derived的虛表揉合到第一張表虛表(classb1的虛表,若classb1無虛表則依次找後面有虛表的作為第一虛表)後。揉合的方法是,把derived新增的虛方法(不是重寫覆蓋基類的那些方法)加到第一張表虛表末尾。
c. 遍歷所有的虛表,替換基類中被derived重寫覆蓋的的方法為基類函式指標。
d. 把基類的成員變數加了整個記憶體布局的最後。
看乙個例項
class b1
; virtual void fun2(){};
int m_b1;
};class b2
; virtual void method2(){};
int m_b2;
};class derived : public b1, public b2
; virtual void method2(){};
virtual void df(){};
int m_b3;
};
derived的記憶體布局構造順序出下(編譯期計算推測)
第a步
第b步
第c步
第d步
經過四步操作,derived記憶體布局已經完成。這裡有個疑問沒有解決:為什麼derived的虛表要揉合到第乙個基類的虛表中去?
解決這個疑惑,我們先來看看如果呼叫
derived* pdd = new derived;
pdd->fun1();
pdd->df();
pdd->method2();
生成的**執行流程是如何的。
呼叫 pdd->fun1();
呼叫 pdd->df();
呼叫 pdd->method2();
可以看到在呼叫派生自第二個基類的方法時,this指標多做了一次偏移計算。
因此可以解釋上面的疑惑:因為c++用得最多的是單繼承,為了在單繼承的時候呼叫基類和子類虛方法不至於偏移指標,就把子類的虛表和第乙個基類虛表進行融合。
根據上述的生成規則。我們可以推導當產生菱形繼承時的布局
class a;
class b : publica; // b的記憶體布局包括了乙份a
class c : publica; // c的記憶體布局包括了乙份a
class d : publicb, public c // d的記憶體布局包括了乙份b和乙份c,因此d間接包含了兩份a
書上說virtual public能解決這個問題。那麼現在總結一下 virtual public的生成規則
規則3.如果乙個類derived從classb1, classb2, ....classbn類多繼承(既有虛的,又有非虛繼承),那麼該類物件的記憶體結構生成過程(這裡是指猜測的編譯時的生成過程而非執行時的過程)是:
等下,在介紹這個之前,我先介紹下乙個類的完整的記憶體布局應該由四部分組成。
我們看到的規則2生成的布局只是「非虛繼承部分」和「derive成員部分」,當引入虛繼承後,每個類的記憶體布局應該有這四部分。下面再說虛繼承的生成過程
a.將所有基類整理順序,非虛繼承基類在前,虛繼承基類在後。
b.先不考慮虛繼承類,將非虛繼承基類和derived類按照規則2進行記憶體布局。但處理過程不包含derived類覆蓋的虛繼承類的方法(這一點vc和gcc的處理可能不一樣,沒有親自實驗gcc),也不包括非虛繼承基類的「虛繼承部分」,也就是說如果非虛繼承基類還虛繼承於另乙個類,那麼非虛繼承基類本內的布局就已經包括了上述三部分,但在參與規則2的計算時,它的「虛繼承部分」暫時不被考慮。
c.在目前已經生成的記憶體布局「非虛繼承部分」和「derive成員部分」之間插入虛基類指標列表vbptr(virtual base table pointer)
d.將所有的虛繼承的基類記憶體布局依次放到步驟c生成的記憶體布局之後(包括步驟b暫時忽略的「虛繼承部分」和本來的虛繼承類,在放入過程中,如果發現有相同的類名,那麼就只放乙份)
e.遍歷「虛繼承部分」中的虛表,把虛表替中被覆蓋的方法換成derived方法。
f.根據「虛繼承部分」中的虛繼承基類的偏移,填充虛基類指標列表vbptr
說了這麼多,看個例子吧
class b1
virtual void fun2()
int m_b1;
};class b2
virtual void method2()
int m_b2;
};class derived : virtual public b1, public b2
virtual void method2()
int m_b3;
};
按照步驟畫出構造圖
第a步,調整順序為
class derived : public b2,virtual public b1
第b步
第c步
第d步
第e步
第f步
根據這個步驟,我們可以解釋在菱形繼承時,如果使用了虛繼承,那麼在步驟d裡面會根據同名型別只有乙份的原則,把多餘的乾掉。
但是這樣一來,我們呼叫來自於虛繼承的介面就就頗費周折,如:
derived*pdd = new derived;
pdd->fun1();
呼叫過程是:取 this指標,計算vbptr的偏移,取vbptr第二項為+8,把+8加在vbptr位址上得到b1vptr,取fun1位址,呼叫。
很複雜。或許你會問,在編譯ppd->fun1()的時候,其實編譯器知道b1 vptr的偏移,為什麼不直接生成找b1 vptr的**?
原因如下,在目前的derived中,b1 vptr的位置是固定的。但是如果把derived作為某類的父類,再虛繼承下,再亂繼承幾下。根據規則3的第d步,在合併之後,你就不知道b1vptr在哪個地方了。而編譯器面對derived指標,必須生成統一的**,這就是必須查vbptr表的原因。
C 記憶體布局
注意,上述只描述的是可執行檔案具有三個段,而不是由該三個段構成。在 linux 下,我們可以通過size命令輸出可執行檔案的段資訊。記憶體布局 存放程式指令和字串常量 我們知道,可執行檔案的文字段包含程式的指令,鏈結器把指令直接從可執行檔案拷貝到記憶體中,形成文字段。存放已初始化的全域性變數和sta...
c 記憶體布局
寫好了 只是第一步,接下來還需要編譯生成對應的二進位制才能使用 預處理,編譯,彙編,鏈結 那麼在執行的時候,和資料在記憶體中都是怎麼分布的呢?c的記憶體布局是怎樣的呢?c 的記憶體布局是怎樣的呢?有一點值得注意,c語言和c 的記憶體布局是不一樣的,這也就是平日裡搜尋c 記憶體布局的文章內容總是很相似...
C語言記憶體布局
重點關注以下內容 c語言程式在記憶體中各個段的組成 c語言程式連線過程中的特性和常見錯誤 c語言程式的執行方式 一 c語言程式的儲存區域 由c語言 文字檔案 形成可執行程式 二進位制檔案 需要經過編譯 彙編 連線三個階段。編譯過程把c語言文字檔案生成匯程式設計序,彙編過程把匯程式設計序形成二進位制機...