【C++】对象实例化/成员函数/成员变量的内存管理
文章目錄
- 1. 對象實例化的內存管理
- 總結
- 2.C++成員函數在內存中的存儲方式
- 3.C++類的實例化對象的大小之sizeof()
- 實例一:
- 實例二:
- 實例三:
- 實例四:
- 實例五:
- 實例六:
- 實例七:
- 實例八:
- 實例九:
1. 對象實例化的內存管理
解釋:
- 因 C++中成員函數和非成員函數都是存放在代碼區的,故類中一般成員函數、友元函數,內聯函數還是靜態成員函數都不計入類的內存空間,測試一和測試二對比可證明這一點
- 測試三中,因出現了虛函數,故類要維護一個指向虛函數表的指針,分別在 x86目標平臺和x64目標平臺下編譯運行的結果可證明這一點
總結
- C++編譯系統中,數據和函數是分開存放的(函數放在代碼區;數據主要放在棧區和堆區,靜態/全局區以及文字常量區也有),實例化不同對象時,只給數據分配空間,各個對象調用函數時都都跳轉到(內聯函數例外)找到函數在代碼區的入口執行,可以節省拷貝多份代碼的空間
- 類的靜態成員變量編譯時被分配到靜態/全局區,因此靜態成員變量是屬于類的,所有對象共用一份,不計入類的內存空間
- **靜態成員函數和非靜態成員函數都是存放在代碼區的,是屬于類的,類可以直接調用靜態成員函數,不可以直接調用非靜態成員函數,兩者主要的區別是有無this指針,**更加詳細的解釋后面專門寫一篇文章
- 內聯函數(聲明和定義都要加inline)也是存放在代碼區,內聯函數在被調用時,編譯器會用內聯函數的代碼替換掉函數,避免了函數跳轉和保護現場的開銷(實際上到底替不替換還要由編譯器決定,即使聲明為內聯函數也有可能不替換,未聲明成內聯函數也有可能被編譯器替換到調用位置,主要由編譯器決定),更詳細的介紹后面也會專門寫一篇文章
2.C++成員函數在內存中的存儲方式
用類去定義對象時,系統會為每一個對象分配存儲空間。如果一個類包括了數據和函數,要分別為數據和函數的代碼分配存儲空間。按理說,如果用同一個類定義了10個對象,那么就需要分別為10個對象的數據和函數代碼分配存儲單元,如下圖所示。
能否只用一段空間來存放這個共同的函數代碼段,在調用各對象的函數時,都去調用這個公用的函數代碼。如下圖所示。
顯然,這樣做會大大節約存儲空間。C++編譯系統正是這樣做的,因此每個對象所占用的存儲空間只是該對象的數據部分(虛函數指針和虛基類指針也屬于數據部分)所占用的存儲空間,而不包括函數代碼所占用的存儲空間。
? 看如下測試代碼
class D
{
public: void printA() { cout<<"printA"<<endl; } virtual void printB() { cout<<"printB"<<endl; }
}; int main(void)
{D *d=NULL;d->printA();d->printB();
}
問題:以上代碼的輸出結果是什么?
? C++程序的內存格局通常分為四個區:全局數據區(data area),代碼區(code area),棧區(stack area),堆區(heap area)(即自由存儲區)。全局數據區存放全局變量,靜態數據和常量;所有類成員函數和非成員函數代碼存放在代碼區;為運行函數而分配的局部變量、函數參數、返回數據、返回地址等存放在棧區;余下的空間都被稱為堆區。根據這個解釋,我們可以得知在類的定義時,類成員函數是被放在代碼區,而類的靜態成員變量在類定義時就已經在全局數據區分配了內存,因而它是屬于類的。對于非靜態成員變量,我們是在類的實例化過程中(構造對象)才在棧區或者堆區為其分配內存,是為每個對象生成一個拷貝,所以它是屬于對象的。
? 應當說明,常說的“某某對象的成員函數”,是從邏輯的角度而言的,而成員函數的存儲方式,是從物理的角度而言的,二者是不矛盾的。
? 下面我們再來討論下類的靜態成員函數和非靜態成員函數的區別:靜態成員函數和非靜態成員函數都是在類的定義時放在內存的代碼區的,因而可以說它們都是屬于類的,但是類為什么只能直接調用靜態類成員函數,而非靜態類成員函數(即使函數沒有參數)只有類對象才能調用呢?
原因是類的非靜態類成員函數其實都內含了一個指向類對象的指針型參數(即this指針),因而只有類對象才能調用(此時this指針有實值)。
? 回答開頭的問題,答案是輸出“printA”后,程序崩潰。類中包括成員變量和成員函數。new出來的只是成員變量,成員函數始終存在,所以如果成員函數未使用任何成員變量的話,不管是不是static的,都能正常工作。需要注意的是,雖然調用不同對象的成員函數時都是執行同一段函數代碼,但是執行結果一般是不相同的。不同的對象使用的是同一個函數代碼段,它怎么能夠分別對不同對象中的數據進行操作呢?原來C++為此專門設立了一個名為this的指針,用來指向不同的對象。
? 需要說明,不論成員函數在類內定義還是在類外定義,成員函數的代碼段都用同一種方式存儲。不要將成員函數的這種存儲方式和inline(內聯)函數的概念混淆。不要誤以為用inline聲明(或默認為inline)的成員函數,其代碼段占用對象的存儲空間,而不用inline聲明的成員函數,其代碼段不占用對象的存儲空間。不論是否用inline聲明(或默認為inline),成員函數的代碼段都不占用對象的存儲空間。用inline聲明的作用是在調用該函數時,將函數的代碼段復制插人到函數調用點,而若不用inline聲明,在調用該函數時,流程轉去函數代碼段的入口地址,在執行完該函數代碼段后,流程返回函數調用點。inline與成員函數是否占用對象的存儲空間無關,它們不屬于同一個問題,不應搞混。
3.C++類的實例化對象的大小之sizeof()
總結一下,C++類的實例化對象的大小之sizeof()。
class D
{
public:D(){} virtual ~D(){}
private:int a ;char *p;
};
實例一:
class A
{
};A a;
cout << sizeof(a) << endl;
運行結果:1
解釋:空類,沒有任何成員變量或函數,即沒有任何存儲內容;但是由A a可知,空類仍然可以實例化。一個類能夠實例化,編譯器就需給它分配內存空間,來指示類實例的地址。**這里編譯器默認分配了一個字節,以便標記可能初始化的類實例,同時使空類占用的空間最少(即1字節)。
實例二:
class B
{
private:int a;
};
B b;
cout << sizeof(b) << endl;
運行結果:4
解釋:當類中有其它成員占據空間時,那一個字節就不算在內了,如本題:結果是4,而不是1+4=5。
實例三:
class BB
{
private:int a ;char b;
};
BB bb;
cout << sizeof(bb) << endl;
運行結果:8
解釋:什么?怎么會是8?不應該是4 + 1 = 5嗎?這里考察了對齊,涉及到編譯器的優化。對于大多數CPU來說,CPU字長的整數倍操作起來更快,因此對于這些成員加起來不夠這個整數倍,有可能編譯器會插入多余的內容湊足這個整數倍;此外,有時候相鄰的成員之間也有可能因為這個目的被插入空白,這個叫做“補齊”(padding)。所以,C++標準緊緊規定成員的排列按照類定義的順序,但是不要求在存儲器中是緊密排列的。因此,如上的一個字節的char在存儲時被補全了,成為了4個字節。
實例四:
class C
{
private:int a ;char *p;
};
C c;
cout << sizeof(c) << endl;
運行結果:8
解釋:一般情況下,如果是指針,則無論指針指向的是什么數據類型,都占4個字節的存儲空間。
實例五:
class D
{
public:D(){}virtual ~D(){}private:int a ;char *p;
};
D d;
cout << sizeof(d) << endl;
運行結果:12
解釋:考察虛函數。當類含有虛函數時,(不論是自己的虛函數,還是繼承來的),那么類中就有一個成員變量信息:虛函數指針(4個字節),這個指針指向一個虛函數表,虛函數表的第一項是類的typeinfo信息,之后的項為此類的所有虛函數的地址。
更進一步的解釋:當類中有虛函數的時候,編譯器會為類建立一個表。這個表就是虛函數表,通過指向虛函數表的指針訪問虛函數表。虛函數表就是為了保存類中的虛函數的地址。我們可以把虛函數表理解成一個數組,數組中的每個元素存放的就是類中虛函數的地址。當調用虛函數的時候,程序不是像普通函數那樣直接跳到函數的代碼處,而是先取出虛表指針即得到虛函數表的地址,根據這個來到虛函數表里,從這個表理取出該函數的指針,最后調用該函數。
實例六:
class E
{
public:E(){}virtual ~E(){}private:int a ;char *p;static int b;
};
E e;
cout << sizeof(e) << endl;
運行結果:12(4+4+4)
解釋:考察靜態成員變量的內存分配。由于靜態成員變量是在靜態存儲區分配空間的,它不屬于實例的一部分,因此類中的static成員變量不占據空間。
實例七:
class F:public E
{
public:F(){}~F(){}private:int c;
};
E e;
cout << sizeof(e) << endl;
運行結果:16(12+4)
解釋:派生類對象的存儲空間 = 基類存儲空間 + 派生類特有的非static數據成員的空間
實例八:
class G: public virtual E
{
public:G(){}~G(){}private:int c;
};
G g;
cout << sizeof(g) << endl;
運行結果:20
解釋:如果是虛繼承的話,類對象的存儲空間大小 = 基類的存儲空間 + 派生類特有的非static數據成員的存儲空間 + 每一個類的虛函數存儲空間(這個是額外加的,即按照這個公式,sizeof(g) = 12(E基類的存儲空間) + 4(G特有的非static數據成員的存儲空間) + 4(E類的虛函數的存儲空間,如果E類中有多個虛函數,只算一次))。
實例九:
class H: public virtual E
{
public:H(){}~H(){}virtual void GetValue(){}private:int c;
};
H h;
cout << sizeof(h) << endl;
運行結果:24
解釋:對比實例八,按照上面的解釋:類對象的存儲空間大小 = 基類的存儲空間 + 派生類特有的非static數據成員的存儲空間 + 每一個類的虛函數存儲空間(sizeof(h) = 12(E基類的存儲空間) + 4(G特有的非static數據成員的存儲空間) + 4(E類的虛函數的存儲空間,如果E類中有多個虛函數,只算一次)+ 4(H類的虛函數的存儲空間,如果H類中有多個虛函數,只算一次))。
如上,就是我對于這種類型的總結,這種問題只能出現一次!!!
總結
以上是生活随笔為你收集整理的【C++】对象实例化/成员函数/成员变量的内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 到美容院做面部护理需要多少钱
- 下一篇: 【C++】C/C++ 中 static