
回答:
(以下大部分都是基于x64編譯器下的windows平臺的gcc version 5.3.0 (GCC)編譯器的測試結(jié)果,不能其他平臺也能得出完全一致的結(jié)論,如果在x32下編譯結(jié)果會指出)
由于class相較于struct,默認的成員就是private,代碼中沒有特地強調(diào)private
-
Fruit的類型大小為32,Apple的類型大小為40。
- 完整測試用代碼:
***http://rextester.com/AUJV82101 ***- 點擊上方連接可以進入全套代碼,點擊左下角的“run”按鈕可以查看運行后的結(jié)果。
- 說明:
- 程序所有的對象均創(chuàng)建在棧中,由系統(tǒng)自動管理,無需手動釋放內(nèi)存
- 完整測試用代碼:
圖示:

注:虛函數(shù)指針因為是一個指針,其大小應(yīng)該為4個字節(jié),但在此我想說,如果使用x64編譯器生成的64位程序的指針大小為8個字節(jié)。(一個只含有虛函數(shù)的struct,x64編譯旗下,虛函數(shù)指針為8字節(jié);x86編譯器上虛函數(shù)指針和普通指針沒啥區(qū)別,都是4個字節(jié))。
在后續(xù)我有詳細的測試論證過程。



關(guān)于答案以下是非常詳細的測試和推理,篇幅較長,感謝您閱讀,希望您多多指正。
答案分析:
- 完整測試用代碼:
***http://rextester.com/AUJV82101 ***- 點擊上方連接可以進入全套代碼,點擊左下角的“run”按鈕可以查看運行后的結(jié)果。
-
代碼運行的初級結(jié)論
代碼初級結(jié)論(x64編譯器的結(jié)果)
- Fruit類和Apple類的相關(guān)定義
class Fruit {
int no;
double weight;
char key;
public:
void print() { }
virtual void process() { }
};
class Apple : public Fruit {
int size;
char type;
public:
void save() { }
virtual void process() { }
};
提出疑問
1 對于Fruit類來說,成員由int、double和char組成,其中,不難由程序員算結(jié)果可知sizeof(int) = 4、sizeof(double) = 8、sizeof(char) = 1,那么1+4+8=13,為何sizeof(Fruit)的結(jié)果為32?
2 對于Apple來說,成員有int、char組成,其中,不難由程序得知sizeof(int) = 4、sizeof(char) = 1,那么1 + 4 = 5,為何sizeof(Apple)的結(jié)果為40呢?
3 這樣定義是否合理,是否存在著內(nèi)存的浪費?
4 內(nèi)存中的額外空間用做了什么?這些空間是否有規(guī)律可循?他們是什么?都占多大的內(nèi)存空間?
......-
分析:
為了弄清楚這些疑問,需要準備一系列的代碼來做實驗。
- 首先我們先來驗證最基礎(chǔ)的一個特點就是內(nèi)存對齊的問題。
- 什么是內(nèi)存對齊。內(nèi)存對齊是** 編譯器 **層面管理的問題,是編譯器管理數(shù)據(jù)位置的一種組織方式。
- 對齊系數(shù)。其實可以把他理解為編譯器的來存放內(nèi)存時,劃分內(nèi)存空間的一把“尺子”。通過這個尺子來量出該怎么劃分內(nèi)存空間。也可以把它理解為切內(nèi)存——這塊蛋糕,所用的最小單位。如果被選中了相應(yīng)的對其系數(shù),那么,也就決定了存放數(shù)據(jù)的內(nèi)存單元的每一行有多寬,所以得出來的內(nèi)存空間大小,一定是對其系數(shù)的倍數(shù)!
- 那么如何來得到對齊系數(shù)呢?
方式一:
程序員可以通過預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”。方式二:
由編譯器自信決定。對于我這次測試的平臺來說,這個編譯器的規(guī)則為,采用成員中最長的變量的長度作為對其系數(shù)。
- 既然知道了對齊系數(shù),那么是否可以幫助解釋之前提出的疑問呢?!
答案是,可以解釋部分內(nèi)容,想要全部弄明白還得等等,我們先來看看這塊能解釋多少吧。
如果這時候那Fruit為例來看,它其中的成員有int,double和char所組成,這三個變量中,最長的應(yīng)該是double了。所以Fruit的大小一定是sizeof(double)的倍數(shù),也就是8的倍數(shù)。目前看,F(xiàn)ruit的大小為32,是符合這個觀點的。那么這三個成員是如何排列呢?
其實他們的安排順序還是狠簡單粗暴的,就是定義變量的順序來組織他們在內(nèi)存中的位置。
比如,F(xiàn)ruit的成員定義順序是int,double,char,則編譯器會先將int按照,對齊系數(shù)放入內(nèi)存中,再看后面的變量,如果,兩者相加小于對齊系數(shù),則放在同一行,如果大于,就單獨再開一行。那么,F(xiàn)ruit的對齊系數(shù)為double的8,sizeof(int)+sizeof(double) > 8,那么double會單獨開一行,放進去。這時候,F(xiàn)ruit的內(nèi)存已經(jīng)為8*2=16了。接下來再看char,由于double單獨為一行,那么char會單獨開一行,所以此時的內(nèi)存為8 * 3 = 24。具體的圖形如下圖:
只考慮成員變量的內(nèi)存圖
關(guān)于這幅圖,int占用了4個內(nèi)存,double占用了8個內(nèi)存單元,char占用了1個內(nèi)存單元。其中紅色的部分為浪費的內(nèi)存空間。
那么說了這么多,到底如何呢,我們接下來用代碼看看。
再看代碼之前,先簡單說明一下代碼的功能
測試定義順序?qū)?nèi)存的影響
the memory of Fruit8---------------------
Address of Fruit8: 0x 0x7ffcdb6a4110 | Size = 24
88 28 40 00 00 00 00 00
00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00
-------------------------------
1 結(jié)果輸出測試類型的名稱
2 結(jié)果輸出該類型的對象的地址和該類型的大小
3 結(jié)果輸出對應(yīng)地址下的內(nèi)容(按字節(jié),以十六進制的方式輸出)
-
類的定義
// Fruit類和Fruit4類之間的區(qū)別主要是定義成員變量的順序 //原始定義 class Fruit { int no; double weight; char key; public: void print() { } virtual void process() { } }; //定義順序調(diào)整(虛函數(shù)同名)(優(yōu)化后) //Fruit的定義成員函數(shù)的順序為從小到大。 class Fruit8 { char key; int no; double weight; public: void print() { } virtual void process() { } }; //定義順序調(diào)整(虛函數(shù)同名)(優(yōu)化后) class Fruit4 { char key; int no; double weight; public: Fruit4(int n, double w, char k) :no(n), weight(w), key(k) {} void print() { } virtual void process() { } }; // 定義了char、char、int、double class Fruit9 { char key; char x; int no; double weight; public: void print() { } virtual void process() { } Fruit9(char a, char b, int n, double w) :key(a), x(b), no(n), weight(w){} }; -
測試代碼
#include <iostream>
#include <string>
#include <iomanip>using namespace std; //為了方便閱讀,這個函數(shù)再次給出,但之后不在贅述 string operator*(string z, int n) { string temp = z; for (int i = 0; i < n; i++) { z += temp; } return z; } //為了方便閱讀,這個函數(shù)再次給出,但之后不在贅述 void printMemo(char* name, void* f, int size) { string s = "-"; cout << "the memory of " << name << s*20 <<"\n"; cout << "Address of "<<name << ": 0x " << hex << f << " | Size = " << dec <<size <<endl; unsigned char* x = (unsigned char*)f; for (int i = 0; i < size; i++) { cout << setfill('0') << setw(2) << hex << (unsigned int)*x << " "; if (!((i + 1) % (size>8? 8: 4))) { cout << "\n"; } x++; } cout << s*30 <<"\n\n"; } int main() { cout << "測試輸出最原始結(jié)構(gòu)" << endl; Fruit f; Fruit* ft = &f; printMemo("Fruit", ft, sizeof(Fruit)); Fruit8 f8; Fruit8* ft8 = &f8; printMemo("Fruit8", ft8, sizeof(Fruit8)); cout << "定義順序調(diào)整(虛函數(shù)同名)(優(yōu)化后)" << endl; Fruit4 f4(1, 4.456, 'c'); Fruit4* ft4 = &f4; printMemo("Fruit4", ft4, sizeof(Fruit4)); Fruit9 f9('a', 'b', 77777777, 1.234); Fruit9* ft9 = &f9; printMemo("Fruit9", ft9, sizeof(Fruit9)); return 0; } -
運行結(jié)果
Fruit的測試結(jié)果
Fruit8的測試結(jié)果
Fruit4的測試結(jié)果
Fruit9的測試結(jié)果
-
結(jié)果分析
-
僅調(diào)整成員定義的順序,F(xiàn)ruit8的大小為24字節(jié),而Fruit的字節(jié)為32字節(jié)。
按照之前的分析,只考慮成員的定義順序,會得到一下的內(nèi)存
修改后的(Fruit4 )內(nèi)存圖
-
-
內(nèi)存輸出的結(jié)果
Fruit4 內(nèi)存輸出結(jié)果- 以上說明了內(nèi)存分布和抽象畫成的一致,但是,觀察可以發(fā)現(xiàn),內(nèi)存空間并不連續(xù),char和int之間并不連續(xù)。因為int如果與char連續(xù)的話,int的內(nèi)存起止的位置都會為奇數(shù),則此時,編譯器會跳過一部分內(nèi)存。為了驗證內(nèi)存跳過的情況,可以比較Fruit4 和Fruit9對比可以看出其內(nèi)存圖分配,就可以看出int內(nèi)存的跳過的情況。其中77777777的十六進制數(shù)為:0x 04 A2 CB 71,a和b的ASCII碼的十六進制數(shù)分別為61和62,因此內(nèi)存情況,可以得到具體內(nèi)存圖。


函數(shù)問題
- 關(guān)于成員屬性在內(nèi)存中是如何分布的基本說明白了,但是,目前還沒有討論完全,因為,F(xiàn)ruit的實際大小為32,我們通過以上理論,解釋了內(nèi)存為24的空間還有8字節(jié)的空間去了哪呢?那么會不會是由于成員函數(shù)而影響的呢?
那么我們先來驗證一下,函數(shù)到底會不會影響類型的大小呢?
二話不說,先上代碼~~~~~
-
先來看看構(gòu)造函數(shù)
//原始定義 class Fruit { int no; double weight; char key; public: void print() { } virtual void process() { } }; //添加構(gòu)造函數(shù)后的定義 class Fruit2 { int no; double weight; char key; public: Fruit2(int n, double w, char k) :no(n), weight(w), key(k) {} void print() { } virtual void process() { } }; -
測試代碼(不含預(yù)先定義的部分,需要請查看上方)
Fruit f; Fruit* ft = &f; printMemo("Fruit", ft, sizeof(Fruit)); cout << "添加構(gòu)造函數(shù)后的定義" << endl; Fruit2 f2(1, 2.345, 'c'); Fruit2* ft2 = &f2; printMemo("Fruit2", ft2, sizeof(Fruit2)); -
運行結(jié)果
- 結(jié)論分析
1 首先可以看出,添加了構(gòu)造函數(shù),并沒有影響類型的內(nèi)存大小,都還是32字節(jié),說明** 構(gòu)造函數(shù),并不影響類型的大小**
2 其次,觀察內(nèi)存空間不難發(fā)現(xiàn),圖中畫雙框的部分的內(nèi)存很相似,而且大小也正是八個字節(jié)的大小,只要研究清楚這個是什么,也就明白了類型的大小到底是怎么一回事。
- 考察虛函數(shù)
二話不說,刷代碼-
代碼
//原始定義 class Fruit { int no; double weight; char key; public: void print() { } virtual void process() { } }; //去掉虛函數(shù)后的定義 class Fruit1 { int no; double weight; char key; public: void print() { } //virtual void process() { } }; -
測試代碼(不含預(yù)先定義的部分,需要請查看上方)
Fruit f; Fruit* ft = &f; printMemo("Fruit", ft, sizeof(Fruit)); cout << "去掉虛函數(shù)后的定義" << endl; Fruit1 f1; Fruit1* ft1 = &f1; printMemo("Fruit1", ft1, sizeof(Fruit1)); -
運行結(jié)果
原始
去掉虛函數(shù) 結(jié)果分析
1 總算發(fā)現(xiàn)了這八個字節(jié)的根本來源——** 虛 函 數(shù) ?。。?*
-
現(xiàn)在知道了一直困擾我們的八個字節(jié)是來自與虛函數(shù)的定義,那么,問題接著就有來了,虛函數(shù)的所占內(nèi)存的大小是多少? 是否遵循對其的原則呢?二話不說,趕快上代碼測試?。?/p>
虛函數(shù)的內(nèi)存問題
-
代碼
//原始定義 class Fruit { int no; double weight; char key; public: void print() { } virtual void process() { } }; //純虛函數(shù)是否影響 class Fruit5 { int no; double weight; char key; public: Fruit5(int n, double w, char k) :no(n), weight(w), key(k) {} void print() { } virtual void process() = 0; }; class Fruit6 { public: Fruit6() {} void print() { } virtual void process() {}; }; //驗證虛函數(shù)和對齊 class Fruit7 { char n; public: Fruit7(char a):n(a) {} void print() { } virtual void process() {}; }; //多個虛函數(shù) class Fruit10 { int no; double weight; char key; public: void print() { } Fruit10(char a, int n, double w) :key(a), no(n), weight(w) {} virtual void process1() { } virtual void process2() { } virtual void process3() { } }; -
測試代碼
Fruit f; Fruit* ft = &f; printMemo("Fruit", ft, sizeof(Fruit)); Apple a; Apple* at = &a; printMemo("Apple", at, sizeof(Apple)); cout << "純虛函數(shù)是否影響" << endl; //抽象類不能創(chuàng)建對象 printMemo("Fruit5", NULL, sizeof(Fruit5)); cout << "測試虛函數(shù)的大小" << endl; Fruit6 f6; Fruit6* ft6 = &f6; printMemo("Fruit6", ft6, sizeof(Fruit6)); cout << "驗證虛函數(shù)和對齊" << endl; Fruit7 f7('a'); Fruit7* ft7 = &f7; printMemo("Fruit7", ft7, sizeof(Fruit7)); cout << "多個虛函數(shù)測試,對齊情況" << endl; Fruit10 f10(1, 10.1056, 'c'); Fruit10* ft10 = &f10; printMemo("Fruit10", ft10, sizeof(Fruit10)); 運行結(jié)果







- 結(jié)論
1 由原始數(shù)據(jù)和Fruit5(純虛函數(shù)的抽象類)的輸出的結(jié)果可以看到,雖然Fruit5不能創(chuàng)建對象,但是不難看出兩者的大小是相同的,所以虛函數(shù)和純虛函數(shù)占用的空間相同
2 由Fruit6的輸出的結(jié)果可以得出,在x86和x64平臺的結(jié)果不相同,*** 在32位平臺的虛函大小為4字節(jié),在64位平臺下的虛函數(shù)的大小為8字節(jié)***
3 由Fruit7可以看出,虛函數(shù)所占內(nèi)存大小的分配規(guī)則,符合對齊的規(guī)則,x64平臺下,虛函數(shù)加char,會浪費7個字節(jié)的空間,x86平臺下會浪費3個字節(jié)。
4 由Fruit10可以看出,多個虛函數(shù)的情況,實際占用與一個虛函數(shù)的情況相同。
現(xiàn)在已經(jīng)完成了類型大小的整理,但是還差一件事,就是父類和子類的關(guān)系。
父類和子類
- 基本版
二話不說上代碼-
代碼
//原始定義 class Fruit { int no; double weight; char key; public: void print() { } virtual void process() { } }; class Apple : public Fruit { int size; char type; public: void save() { } virtual void process() { } }; //去掉虛函數(shù)后的定義 class Fruit1 { int no; double weight; char key; public: void print() { } //virtual void process() { } }; class Apple1 : public Fruit1 { int size; char type; public: void save() { } //virtual void process() { } }; -
測試代碼
cout << "測試輸出最原始結(jié)構(gòu)" << endl; Fruit f; Fruit* ft = &f; printMemo("Fruit", ft, sizeof(Fruit)); Apple a; Apple* at = &a; printMemo("Apple", at, sizeof(Apple)); -
輸出結(jié)果
分析
1 Apple為Fruit的子類,并且Fruit具有三個可能影響類型大小的成員,分別為int、char、虛函數(shù)。其中int為4字節(jié),char為1字節(jié),對齊系數(shù)為4,那么成員屬性的大小為8字節(jié)。Fruit的大小為32字節(jié),Apple的大小為40字節(jié)。
2 對于去除了虛函數(shù)的情況,包含虛函數(shù)的父類和子類大小分別為32、40,去除掉后,大小分別為24, 32。相當(dāng)于每個類減少了8個字節(jié)(父類的對齊系數(shù))結(jié)論
-
- 子類會繼承父類的對齊系數(shù),子類的成員是依據(jù)父類的對齊系數(shù)來計算的
- 子類的虛函數(shù),不對大小產(chǎn)生影響
- 父類在子類中的位置
- 代碼
class Fruit2 {
int no;
double weight;
char key;
public:
Fruit2(int n, double w, char k) :no(n), weight(w), key(k) {}
void print() { }
virtual void process() { }
};
class Apple2 : public Fruit2 {
int size;
char type;
public:
Apple2(int s, char t, int n, double w, char k) :size(s), type(t), Fruit2(n, w, k) {}
void save() { }
virtual void process() { }
};
//定義順序調(diào)整(虛函數(shù)同名)(優(yōu)化后)
class Fruit4 {
char key;
int no;
double weight;
public:
Fruit4(int n, double w, char k) :no(n), weight(w), key(k) {}
void print() { }
virtual void process() { }
};
class Apple4 : public Fruit4 {
char type;
int size;
public:
Apple4(int s, char t, int n, double w, char k) :Fruit4(n, w, k), size(s), type(t){}
void save() { }
virtual void process() { }
};
-
測試代碼
cout << "測試元素分布" << endl;
//驗證位置(未優(yōu)化1)
Fruit2 f21(1, 2.345, 'c');
Fruit2* ft21 = &f21;
printMemo("Fruit2", ft21, sizeof(Fruit2));//驗證位置(未優(yōu)化2) Fruit2 f22(3, 2.345, 'b'); Fruit2* ft22 = &f22; printMemo("Fruit2", ft22, sizeof(Fruit2)); //驗證位置(未優(yōu)化1) Apple2 a21(9, 'd', 2, 6.789, 'e'); Apple2* at21 = &a21; printMemo("Apple2", at21, sizeof(Apple2)); //驗證位置(未優(yōu)化2) Apple2 a22(8, 'a', 3, 6.789, 'f'); Apple2* at22 = &a22; printMemo("Apple2", at22, sizeof(Apple2)); //驗證位置(優(yōu)化后1) Fruit4 f41(1, 41.4156, 'c'); Fruit4* ft41 = &f41; printMemo("Fruit41", ft41, sizeof(Fruit4)); //驗證位置(優(yōu)化后2) Fruit4 f42(2, 41.4156, 'b'); Fruit4* ft42 = &f42; printMemo("Fruit42", ft42, sizeof(Fruit4)); Apple4 a41(9, 'd', 1, 41.4156, 'c'); Apple4* at41 = &a41; printMemo("Apple41", at41, sizeof(Apple4)); Apple4 a42(8, 'e', 1, 41.4156, 'g'); Apple4* at42 = &a42; printMemo("Apple42", at42, sizeof(Apple4)); 運行結(jié)果


- 分析




- 結(jié)論
由之前的分析可以看出來,對于子類來說,虛函數(shù)指針是相同的位置,子類成員所占的內(nèi)從空間始終在父類之后,父類空間后面所剩下的位置,可以與子類共用
- 子類與虛函數(shù)(多個,同名(override)與不同名的虛函數(shù)的關(guān)系)
-
代碼
//多個虛函數(shù) class Fruit10 { int no; double weight; char key; public: void print() { } Fruit10(char a, int n, double w) :key(a), no(n), weight(w) {} virtual void process1() { } virtual void process2() { } virtual void process3() { } }; class Apple10 : public Fruit10 { int size; char type; public: Apple10(char t, int s, char a, int n, double w) :Fruit10(a, n, w), type(t), size(s) {} void save() { } virtual void process1() { } virtual void process2() { } virtual void process4() { } }; -
測試代碼
cout << "多個虛函數(shù)測試,對齊情況" << endl; Fruit10 f10(1, 10.1056, 'c'); Fruit10* ft10 = &f10; printMemo("Fruit10", ft10, sizeof(Fruit10)); Apple10 a10(9, 'd', 1, 10.1056, 'c'); Apple10* at10 = &a10; printMemo("Apple10", at10, sizeof(Apple10)); -
輸出結(jié)果
結(jié)論
-
子類的虛函數(shù)所站類型的大小,和數(shù)量,是否同名無關(guān),始終處于最上方,且大小固定(為一個對齊系數(shù)的大?。?/strong>
虛表問題
- 之前的部分,基本把這個問題講清楚了,但還留下了一個問題:為什么多個虛函數(shù),也只用一個指針就夠了???(由于此處重點非虛表,所以不做詳細說明。)
關(guān)于這個問題,我在這里簡單解釋一下,虛函數(shù)在類中,只需要保存一個指針即可,那么這個指針所指向的內(nèi)容就很重要了,它會指向一個數(shù)組,在數(shù)組中保存著他所持有的虛函數(shù)即可,這樣他只需要持有一個固定大小的指針就行了,而不需要考慮實際擁有幾個虛函數(shù)的問題。但是,對于虛函數(shù)來說,還有一個更大的用途,那就是對于實現(xiàn)父類和子類中的虛函數(shù)的關(guān)系騎著非常重要的作用了。由之前的測試程序可以看出,對于各相同類型的不同對象,實際虛函數(shù)指針所指的區(qū)域是相同的。(比如Fruit21 f21和Fruit22 f22等),也就是虛表實際只和class相關(guān),具體的對象只需指向這塊內(nèi)存空間即可。而編譯器,實際在調(diào)用虛函數(shù)f21.xxVirtualFunction();時,實際編一起會將其轉(zhuǎn)化為(* (f21 -> vptr)[n])(f21); 或 (*f21->vptr[n])(f21);來進行執(zhí)行。可以看出實際是從數(shù)組中取出對應(yīng)函數(shù)的指針,并將對象傳入其中進行調(diào)用的過程。此時該數(shù)組中的元素指向,如果是存在子父類關(guān)系的同名虛函數(shù)(子類override父類虛函數(shù)的情況)的情況,虛表中的指針所指的虛函數(shù)的指針為同一個!這樣的設(shè)計好處,由于對象是由參數(shù)傳入的,所以能夠輕松實現(xiàn)多態(tài)。












