關於C 的類物件,記憶體分布問題

2021-04-30 10:00:43 字數 4566 閱讀 4689

原問題如下:

#include

using namespace std;

class base

void fun2()

};int main()

**的結果為4。

這個我覺得是因為那個虛函式表裡的乙個指標佔了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開發的過程中,老是關注不到這個細節,自己做的時候就出錯。這類問題還很多。

這在開發中,表現出來的就是,嚮導的**永遠正確,自己的手工**一寫就錯,慢慢弄下來,搞得很多人都不敢手工寫**了。

我原來寫文章建議,初學者不要用ide,其實就是指,這些細節被ide自動完成,程式設計師關注不到,就學不到真東西,搞得哪天一手寫,就出錯。

呵呵,扯遠了,虛函式的繼承關係,我知道的大概就這麼點,已經全部兜給你了,再不夠,只有請你自己查書了。

呵呵,好累。

C 類記憶體分布

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

關於C 中父類指標引用物件在記憶體中的分布

通過程式,這是我自己的理解,如果不對,麻煩幫忙指出來 程式1 include using namespace std class classa virtual void functiona int a int b class classb public classa int a int b clas...

C 中類的記憶體分布

如何計算類物件占用的位元組數?乙個空類的話1個位元組。這是為了保證n個連續空類宣告時,變數位址有偏移,防止變數覆蓋。非空類的話用關鍵字sizeof計算。如果手工計算就相當麻煩,光padding就一堆規則了。而且有些額外資訊比如虛函式 多個虛函式也只產生乙個vptr指標 等等。乙個類成員 當有虛函式時...