我們來看一下C++類實例化的時候,它的各個成員在內存中的分布是怎么樣的。這個問題看似簡單,其實還是有許多情形需要考慮的:比如說類中是否有虛函數,子類與基類的實例內存結構有何區別,C/C++的內存對齊(比如4字節對齊或8字節對齊)對類的實例大小及內存分布有何影響? 一個空類的大小又是多少呢?
我們先看一個沒有虛函數的類Fruit及它的子類Apple:
class Fruit{ int _no; double _weight; char _key;public: Fruit(int no, double weight, char key): _no(no), _weight(weight), _key(key) {}};class Apple: public Fruit{ int _size; char _type;public: Apple(int no, double weight, char key, int size, char type): Fruit(no, weight, key), _size(size), _type(type) {}};在Code::Blocks 8.02中運行如下代碼:
Fruit f1(1, 2.3, 'F'); Apple a1(2, 3.4, 'B', 5, 'A'); cout<<"sizeof(Fruit)="<<sizeof(f1)<<endl; cout<<"sizeof(Apple)="<<sizeof(a1)<<endl;結果如下: sizeof(Fruit)=24 sizeof(Apple)=32
通過查看memory,可知f1和a1內存結構如下圖:
可以看出這里編譯器默認采用8字節對齊。在Apple對象中,Fruit的部分剛好位于其頂部。Apple的成員size跟Fruit的成員key及填充共用一個8字節。
我們可以看出一下幾點: 1. 子類的實例包含了基類的部分,并且基類的部分位于子類實例的開始部分; 2. 在沒有虛函數的時候,C++類的大小只與其數據成員有關, 它有沒有聲明函數或者在類中實現函數都不影響類的大小。這個其實很好理解,因為類里面函數的代碼對于該類的每個實例都是一樣的,所以它不是放在類的實例中,而是放在代碼段中,否則同一個類的每個實例都會額外占用大量內存。
下面我們再看一下有虛函數的情況。我們都知道C++的類有虛函數的時候, 類的object的會多一個vptr指針,指向vtbl。那么是不是一個類有了虛函數之后, 它的size就會增加4呢?
我們把上面兩個類都加上虛函數PRocess(),新的代碼如下:
class Fruit{ int _no; double _weight; char _key;public: Fruit(int no, double weight, char key): _no(no), _weight(weight), _key(key) {} virtual void process(){cout<<"Fruit::process()"<<endl;}};class Apple: public Fruit{ int _size; char _type;public: Apple(int no, double weight, char key, int size, char type): Fruit(no, weight, key), _size(size), _type(type) {} virtual void process(){cout<<"Apple::process()"<<endl;}};`重新編譯。在Code::Blocks 8.02下運行結果為:
sizeof(Fruit)=24 sizeof(Apple)=32
那為什么加了vptr,類實例的大小不變呢? 根據查看memory,可知Fruit和Apple(有虛函數)的實例內存分配如下圖:
我們可以看出,Fruit和Apple類的實例的vptr位于最開始的位置,并且vptr和Fruit類的no合在一起組成一個8字節。
下面談談怎么敢斷定頭4個字節就是vptr呢? 事實上我們可以通過f1或a1的頭4個字節,看它指向什么地址,它指向的地址我們猜想應該是第一個虛函數的函數指針,我們通過這個函數指針來調用這個函數,看看是不是會打印出相應信息。
以f1為例: &f1 - 0x28ff10, f1的地址。 (int *)(&f1) - 0x28ff10, f1的地址轉換為int指針。 *(int *)(&f1) - 0x4452b0, f1的地址轉換為int,這就是vptr的值。 (int*)*(int*)(&f1) - 0x4452b0, vptr指向的內容轉換為int指針,它指向vtbl的第一項。 *(int*)*(int*)(&f1) - 0x41596c, vtbl第一項對應的值,它是一個指針,指向一個函數。我們下面會把它轉換成函數指針。
通過我們上面得到的指針,我們就可以調用這個函數了,看它是不是真的打印出了Fruit::process(),測試代碼如下:
重新編譯,果然打印出了”Fruit::process()”。證明Fruit的頭4個字節就是它的vptr。
注意,我們也可以通過(*pf)()來調用這個函數。因為調用fun()和(*fun)()是等價的。這里為什么函數指針加不加*都可以調用呢?其實編譯器這里很清楚知道是要調用fun這個函數,所以兩種寫法都可以。但是如果是指針指向一個變量就不一樣了,編譯器不知道你是要訪問這個變量還是它的地址。
用同樣的方法(只需將上面的f1換成a1),我們也可以直接通過a1的頭4個字節得到其vptr,從而call a1的虛函數。結果打印出了”Apple::Process()”。這樣我們也驗證了a1的頭4個字節確實是它的vptr。
再考慮一下,如果Fruit有虛函數,Apple沒有定義新的虛函數,也沒有override Fruit的虛函數,那Apple的實例的內存分布如何呢?還會有vptr嗎?
class Fruit{ int _no; double _weight; char _key;public: Fruit(int no, double weight, char key): _no(no), _weight(weight), _key(key) {} virtual void process(){cout<<"Fruit::process()"<<endl;}};class Apple: public Fruit{ int _size; char _type;public: Apple(int no, double weight, char key, int size, char type): Fruit(no, weight, key), _size(size), _type(type) {}};重新編譯,發現a1的size仍然為32。通過查看memory發現其內存分布與上圖是一樣的。通過頭4個字節vptr我們找到vtbl的第一項,將其轉換為函數指針后調用,我們發現其打印出了”Fruit::process()”。可見,如果基類有虛函數,子類沒有定義新的虛函數,也沒有對基類虛函數override的話,子類的實例仍然會有vptr,其指向一個vtbl,該vtbl的每一項都從基類的vtbl的相應項拷貝而來。
再思考一個問題,如果是一個空類,其實例的size是否為0呢?如果非0,其內容是什么?
class A{};A a;cout<<"sizeof(a)"<<siszeof(a)<<endl;測試發現a的大小為1,通過查看memory發現該字節內容為0。可見C++編譯器對于空類為了能讓其實例化,仍然會給它分配一個字節的內存。注意單獨對于a而言,C/C++編譯器的sizeof()不會考慮內存對齊問題,但是如果a又是其他類的一部分,則就要考慮內存對齊了。
class B{ A a; int c;};B b;cout<<"sizeof(b)"<<siszeof(b)<<endl;通過測試,sizeof(b)=8。可見空類仍然參與了字節對齊。
新聞熱點
疑難解答
圖片精選