C 記憶體布局生成步驟

2021-06-05 19:20:27 字數 4394 閱讀 8750

對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語言文字檔案生成匯程式設計序,彙編過程把匯程式設計序形成二進位制機...