關於C++的類對象,內存分佈問題

原問題如下:

#include <iostream>
using namespace std;
class base{
public:
 virtual void fun1()
 {
 cout << "fun1 called !" << endl;
 }
 void fun2()
 {
 cout << "fun2 called !" << endl;
 }
};
int main()
{
    base s;
    cout << sizeof(s) << endl;
    return 0;
}
代碼的結果爲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
{
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開發的過程中,老是關注不到這個細節,自己做的時候就出錯。這類問題還很多。
這在開發中,表現出來的就是,嚮導的代碼永遠正確,自己的手工代碼一寫就錯,慢慢弄下來,搞得很多人都不敢手工寫代碼了。
我原來寫文章建議,初學者不要用IDE,其實就是指,這些細節被IDE自動完成,程序員關注不到,就學不到真東西,搞得哪天一手寫,就出錯。
呵呵,扯遠了,虛函數的繼承關係,我知道的大概就這麼點,已經全部兜給你了,再不夠,只有請你自己查書了。
呵呵,好累。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章