類對象記憶體分布

來源:互聯網
上載者:User

標籤:

================================================================================================如何計算類對象佔用的位元組數?

一個空類的話1個位元組。這是為了保證n個連續空類聲明時,變數地址有位移,防止變數覆蓋。 
非空類的話用關鍵字sizeof計算。
如果手工計算就相當麻煩,光padding就一堆規則了。而且有些額外資訊比如虛函數(多個虛函數也只產生一個vptr指標)等等。
一個類成員 ,當有虛函數時,有以下成分:各個資料成員,資料對齊產生的間隙,一個虛函數表的 "指標"(無虛不存在)。
建構函式不能用 memset(this, 0, sizeof(*this))) 初始化。原因就是每個類裡面除了資料成員之外 ,還有一個虛函數表指標 。memcpy另一個同類型類的執行個體內容過去倒是可以 ,這種情況下該函數表指標可以正確複製過去。
注意虛函數表只在類有虛函數的情況下才存在, 沒有虛函數不存在。 建構函式悄悄地幫你設定虛函數表的內容,並把正確的指標存放在對象中。 

類在記憶體中分布

首先請看程式:

#include <iostream>
using namespace std;
class base{ 
public: 
virtual void fun1() 
//void fun1()
{ 
cout << "fun1 called !" << endl; 
} 
void fun2() 
{ 
cout << "fun2 called !" << endl; 
} 
}; 
int main() 
{ 
base s; 
cout << sizeof(s)<< endl; 
return 0; 
} 
代碼的結果為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 
{ 
virtual void Func(void) 
}; 
class B : public A 
{ 
virtual void Func(void) 
}; 
class C : public B 
{ 
virtual void Func(void) 
}; 
這個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 
{ 
// 構造 
public: 
CtestDlg(CWnd* pParent = NULL); // 標準建構函式
// 對話方塊資料 
enum { IDD = IDD_TEST_DIALOG };
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() 
{ 
CDialog::OnInitDialog(); //看這句,在幹嗎?
// 將“關於...”功能表項目添加到系統功能表中。
// IDM_ABOUTBOX 必須在系統命令範圍內。 
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); 
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE); 
if (pSysMenu != NULL) 
{ 
CString strAboutMenu; 
strAboutMenu.LoadString(IDS_ABOUTBOX); 
if (!strAboutMenu.IsEmpty()) 
{ 
pSysMenu->AppendMenu(MF_SEPARATOR); 
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); 
} 
}
// 設定此對話方塊的表徵圖。當應用程式主視窗不是對話方塊時,架構將自動 
// 執行此操作 
SetIcon(m_hIcon, TRUE); // 設定大表徵圖 
SetIcon(m_hIcon, FALSE); // 設定小表徵圖
// TODO: 在此添加額外的初始化代碼
return TRUE; // 除非將焦點設定到控制項,否則返回 TRUE 
}
注意到沒,由於繼承類的虛函數一旦實現,父類的虛函數就被自動屏蔽,VC也必須手動實現對父類虛函數的層級調用,才能完成準系統。 
很多時候,我們的同學,手動繼承一個類之後,玩虛函數老是忘了這個手動調用父類,結果發現,虛函數功能越繼承越少,甚至繼承到功能沒有了,就是搞忘了這點。 
但是,上述代碼是VC的嚮導自動添加的,VC並沒有對此作顯式說明,結果,大家在只用IDE開發的過程中,老是關注不到這個細節,自己做的時候就出錯。這類問題還很多。================================================================================================

類對象記憶體分布

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.