C 中類的記憶體分布

2021-07-23 12:01:18 字數 4833 閱讀 4549

如何計算類物件占用的位元組數?

乙個空類的話1個位元組。

這是為了保證n個連續空類宣告時,變數位址有偏移,防止變數覆蓋。 

非空類的話用關鍵字sizeof計算。

如果手工計算就相當麻煩,光padding就一堆規則了。而且有些額外資訊比如虛函式(多個虛函式也只產生乙個vptr指標)等等。

乙個類成員 ,當有虛函式時,有以下成分:各個資料成員,資料對齊產生的間隙,乙個虛函式表的 "指標"(無虛不存在)。

建構函式不能用 memset(this, 0, sizeof(*this))) 初始化。原因就是每個類裡面除了資料成員之外 ,還有乙個虛函式表指標 。memcpy另乙個同型別類的例項內容過去倒是可以 ,這種情況下該函式表指標可以正確複製過去。

注意虛函式表只在類有虛函式的情況下才存在

, 沒有虛函式不存在。 建構函式悄悄地幫你設定虛函式表的內容,並把正確的指標存放在物件中。 

類在記憶體中分布

首先請看程式:

#include

using namespace std;

class base 

void fun2() 

}; int main() 

**的結果為4,這是由於虛指標的存在。 但是如果去掉virtual **的結果為1。

為什麼呢?

這個涉及到類和結構體,在c++內部的排列方式。 

我們知道,c和c++雖然都支援結構體,但是,實際上表現是不一樣的。

c++的結構體,可以認為是類的一種變體,二者的差異性,類中成員,如果不宣告,預設是private的,結構體中成員,如果不宣告,則預設是public的。 

但是,在c++裡面,二者內部都可以內建成員函式,而c的結構體,內部只允許存在成員變數,如果需要內建成員函式,需要程式設計師顯式宣告函式指標變數,換句話說,就是c在結構體中管理成員函式,是程式設計師自己來管理,c++則是編譯器代為管理。 

這意味著什麼呢? 

在c++中,成員函式和成員變數,都是類和結構體的成員,但二者有所差異。 

編譯器在編譯每個類時,不管這個類以後會例項化幾個物件,首先,它會提取這些類的共性,放到一起,做成乙個表。 

比如類裡面的非虛函式,這類函式,所有的物件共享一段函式**,自然沒有必要每個物件內部都設定乙個函式指標,這太浪費記憶體了。 

因此,乙個類,所有的非虛函式,會被編譯器排成乙個符號表,放置在特定的編譯期基礎變數區。這實際表現看,是放在exe檔案裡面的,在呼叫乙個程式時,是直接從檔案中讀出,並經過位址修訂,準備使用,這部分連基棧都算不上,算是常量區了,所有的常量也是放在這個區。 

嗯,函式內部的靜態變數,類中的靜態變數,靜態函式,都是這個區。 

那,除掉這些,類裡面還有什麼呢? 

還有虛函式,我們知道,虛函式表示可能繼承,事實上,多次(不是多重)繼承後,乙個類的虛函式內部會有乙個棧,每個虛函式都有乙個棧,每次呼叫該函式,會從棧頂開始call,當然,如果程式設計師願意,也可以在繼承的虛函式內部,通過呼叫父類的同名虛函式,逐級向下call,直至call完所有的虛函式為止。 

這就說明,虛函式和普通成員函式不同,每個物件都有可能變化,因此,

編譯器就不敢把這個函式的指標,放在常量區,必須跟著物件走

,注意,不是類,

類是沒有實體的,因此,不存在sizeof,只有物件存在大小。 

還有就是普通成員變數,這些內容,每個物件也是不一樣的,因此,每個物件必須自己建立乙個表來管理,否則大家就混了。 

因此,我們知道了,每個類,例項化物件之後,其實物件的實體在記憶體中的儲存,就只包含虛函式和普通成員變數,這是c++編譯器為了節約記憶體做得優化。 

我們回到你的**看,你的**中,fun2是普通函式,被編譯器放到常量區去了,因此,不占用物件空間,虛函式fun1,則需要占用,我們知道,32位作業系統,乙個指標是4bytes,函式指標也是指標,因此,你的結果是4bytes。

取消了virtual 之後,fun1也變成了普通函式,因此和fun2等同處理,就不再占用物件空間,因此,物件空間為0了。 

不過,我隱隱約約聽誰說過,c++語言不允許物件空間為0,這樣的話,物件指標就沒有落點了,因此,乙個物件的空間,至少占用1byte,這就是你的結果為1的原因。 

虛函式呼叫的幾點補充說明:

類的虛函式,實際上內部儲存上,表現為乙個函式指標棧,棧底,是基類這個函式的指標,往上,實際上是繼承類,該虛函式的繼承函式的指標,乙個類,被繼承幾次,比如說3次,最後一次繼承,這個棧就有3層。有點繞。

舉個例子吧 

class a 

; class b : public a 

; class c : public b 

; 這個a類,裡面的func指標就是它自己 

b就是乙個棧了,棧底是a::func,棧頂是b::func 

而c就是三層的棧了,在b的基礎上,棧頂又壓入了c::func 

基本上就是這個管理關係。 

我的話的意思是,在任何一層繼承函式,都可以去手動去call父類的對應函式,完成對整個棧鏈上所有函式的呼叫。

因為我們知道,乙個類的虛函式,一旦被繼承,原來的父類函式指標就被壓倒棧下面去了,從棧頂看,只有最後一層的函式指標。 

比如c這個類看,我們看它的func,只要它繼承並實現了,那麼,呼叫func一定只能呼叫c::func,b和a的由於看不到,因此是不會被呼叫的。 

當然,如果c沒有實現這個虛函式,則func的棧上,沒有c::func,因此,直接call會call到b::func,以此類推,如果b沒有實現這個虛函式,表示未繼承,則call會call到a::func,這就是虛函式繼承中,後實現的覆蓋前實現的原理。 

當然,如果a內沒有實現func的實體,做了乙個純虛函式,而b和c這些繼承類也不實現,那麼,編譯器在構造符號表的時候,就會找不到任何乙個func的實體,該虛函式棧為空,無法連線,因此會報連線失敗的錯誤,編譯不能通過。

這種棧式管理,有好有壞,好處是後面的繼承類,可以選擇實現虛函式,也可以選擇不實現,偷個懶。程式不會出錯,下次呼叫該函式,會自動沿著它的繼承關係,尋找父類以及更往前的爺爺類的函式實體,至少能找到乙個執行其功能,簡化開發。 

但是,也有乙個壞處,就是乙個虛函式,一旦被繼承類實現了,則父類的必然被覆蓋,如果父類有什麼內建的功能,就沒有辦法執行了,這很麻煩,由於物件導向的繼承關係,我們總是希望,繼承類的對應函式,只要完成它相對於父類增加的那部分功能就夠了,父類的功能,還能繼續執行,免得寫重複的**。 

這個例子在mfc開發中很多,很多時候,我們的乙個視窗類,是從cdialog這個類繼承的,而cdialog,又是cwnd這個類繼承的。針對乙個虛函式方法,比如說cwnd::create這個方法。 

virtual bool create( lpctstr lpszclassname, lpctstr lpszwindowname, dword dwstyle, const rect& rect...

我們知道,建立乙個視窗有一大堆事情要做,這些事情,mfc已經在cwnd的create這個函式裡面實現好了,但好死不死,它把這個函式方法設定為虛函式了,就是說,後續繼承類可以自己來實現這個方法。 

我們這麼來假設,如果我們那個工程的視窗,繼承自cdialog,然後,我們自己實現了這個create方法,那完蛋了,由於c++這個覆蓋特性,執行的時候,就只執行我們這個create了,下面的cdailog::create和cwnd::create都執行不了,除非我們把那兩個函式內部所有的**抄一遍,否則,這個create根本沒有辦法完成我們希望完成的功能。他失去了建立視窗的功能。 

因此,為了解決這個問題,c++允許繼承類的虛函式,顯式呼叫父類的虛函式,以實現父類的基礎功能,最後,才是我們自己新增加的**。 

這個意思主要是說,虛函式的繼承,看似省事,但他不是想當然會先實現父類功能,後呼叫新增**,需要我們手動call。 

再看看這個例子,我們以vc建立乙個mfc的對話方塊工程,就叫test。

// ctestdlg 對話方塊 

class ctestdlg : public cdialog 

;protected: 

virtual void dodataexchange(cdataexchange* pdx); // ddx/ddv 支援

// 實現 

protected: 

hicon m_hicon;

// 生成的訊息對映函式 

virtual bool oninitdialog(); //看好這一句啊,虛函式 

afx_msg void onsyscommand(uint nid, lparam lparam); 

afx_msg void onpaint(); 

afx_msg hcursor onquerydragicon(); 

declare_message_map() 

}; 注意其中oninitdialog,好,我們來看看vc自動為我們生成的這個函式是怎麼寫的: 

bool ctestdlg::oninitdialog()  }

// 設定此對話方塊的圖示。當應用程式主視窗不是對話方塊時,框架將自動 

// 執行此操作 

seticon(m_hicon, true); // 設定大圖示 

seticon(m_hicon, false); // 設定小圖示

// todo: 在此新增額外的初始化**

return true; // 除非將焦點設定到控制項,否則返回 true 

}注意到沒,由於繼承類的虛函式一旦實現,父類的虛函式就被自動遮蔽,vc也必須手動實現對父類虛函式的層級呼叫,才能完成基本功能。 

很多時候,我們的同學,手動繼承乙個類之後,玩虛函式老是忘了這個手動呼叫父類,結果發現,虛函式功能越繼承越少,甚至繼承到功能沒有了,就是搞忘了這點。 

但是,上述**是vc的嚮導自動新增的,vc並沒有對此作顯式說明,結果,大家在只用ide開發的過程中,老是關注不到這個細節,自己做的時候就出錯。這類問題還很多。 

C 類記憶體分布

成員變數依據宣告的順序進行排列 類內偏移為0開始 成員函式不佔記憶體空間 上部分為成員變數,下部分為虛表 當建立乙個含有虛函式的父類的物件時,編譯器在物件構造時將虛表指標指向父類的虛函式 同樣,當建立子類的物件時,編譯器在建構函式裡將虛表指標 子類只有乙個虛表指標,它來自父類 指向子類的虛表 這個虛...

類中成員記憶體分布

static修飾成員變數 對於非靜態資料成員,每個類物件都有自己的拷貝。而靜態資料成員被當做是類的成員,無論這個類被定義了多少個,靜態資料成員都只有乙份拷貝,為該型別的所有物件所共享 包括其派生類 所以,靜態資料成員的值對每個物件都是一樣的,它的值可以更新。因為靜態資料成員在全域性資料區分配記憶體,...

C 中的記憶體分布 經典

c 中,記憶體分為5個區 堆 棧 自由儲存區 全域性 靜態儲存區和常量儲存區。棧 是由編譯器在需要時自動分配,不需要時自動清除的變數儲存區。通常存放區域性變數 函式引數等。堆 是由new分配的記憶體塊,由程式設計師釋放 編譯器不管 一般乙個new與乙個delete對應,乙個new與乙個delete對...