C++類的大小計算匯總
C++中類涉及到虛函數成員、靜態成員、虛繼承、多繼承、空類等。
類,作為一種類型定義,是沒有大小可言的。
類的大小,指的是類的對象所占的大小。因此,用sizeof對一個類型名操作,得到的是具有該類型實體的大小。
- 類大小的計算,遵循結構體的對齊原則;
- 類的大小,與普通數據成員有關,與成員函數和靜態成員無關。即普通成員函數、靜態成員函數、靜態數據成員、靜態常量數據成員,均對類的大小無影響;
- 虛函數對類的大小有影響,是因為虛函數表指針帶來的影響;
- 虛繼承對類的大小有影響,是因為虛基表指針帶來的影響;
- 靜態數據成員之所以不計算在類的對象大小內,是因為類的靜態數據成員被該類所有的對象所共享,並不屬於具體哪個對象,靜態數據成員定義在內存的全局區;
- 空類的大小(類的大小為1),以及含有虛函數,虛繼承,多繼承是特殊情況;
- 計算涉及到內置類型的大小,以下所述結果是在64位gcc編譯器下得到(int大小為4,指針大小為8);
一、簡單情況的計算
#include<iostream> using namespace std; class base { public: base()=default; ~base()=default; private: static int a; int b; char c; }; int main() { base obj; cout<<sizeof(obj)<<endl; }
計算結果:8(靜態變量a不計算在對象的大小內,由於字節對齊,結果為4+4=8)。
二、空類的大小
C++的空類是指這個類不帶任何數據,即類中沒有非靜態(non-static)數據成員變量,沒有虛函數(virtual function),也沒有虛基類(virtual base class)。
直觀地看,空類對象不使用任何空間,因為沒有任何隸屬對象的數據需要存儲。然而,C++標準規定,凡是一個獨立的(非附屬)對象都必須具有非零大小。換句話說,c++空類的大小不為0 。
#include <iostream> using namespace std; class NoMembers { }; int main() { NoMembers n; cout << sizeof(n) << endl; }
計算結果1。
C++標準指出,不允許一個對象(當然包括類對象)的大小為0,不同的對象不能具有相同的地址。
這是由於:
- new需要分配不同的內存地址,不能分配內存大小為0的空間;
- 避免除以 sizeof(T)時得到除以0錯誤;
故使用一個字節來區分空類。
但是,有兩種情況值得我們註意
第一種情況,空類的繼承:
當派生類繼承空類後,派生類如果有自己的數據成員,而空基類的一個字節並不會加到派生類中去。
class Empty {}; struct D : public Empty { int a;};
sizeof(D)為4。
第二種情況,一個類包含一個空類對象數據成員:
class Empty {}; class HoldsAnInt { int x; Empty e; };
sizeof(HoldsAnInt)為8。
在這種情況下,空類的1字節是會被計算進去的。而又由於字節對齊的原則,所以結果為4+4=8。
繼承空類的派生類,如果派生類也為空類,大小也都為1。
三、含有虛函數成員的類
虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實現的。編譯器必需要保證虛函數表的指針存在於對象實例中最前面的位置(這是為了保證正確取到虛函數的偏移量)。
每當創建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就會為這個類創建一個虛函數表(VTABLE)保存該類所有虛函數的地址,其實這個VTABLE的作用就是保存自己類中所有虛函數的地址,可以把VTABLE形象地看成一個函數指針數組,這個數組的每個元素存放的就是虛函數的地址。在每個帶有虛函數的類中,編譯器秘密地置入一指針,稱為vpointer(縮寫為VPTR),指向這個對象的VTABLE。 當構造該派生類對象時,其成員VPTR被初始化指向該派生類的VTABLE。所以可以認為VTABLE是該類的所有對象共有的,在定義該類時被初始化;而VPTR則是每個類對象都有獨立一份的,且在該類對象被構造時被初始化。
class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } };
當定一個Base類的實例b時,其b中成員的存放如下:
指向虛函數表的指針在對象b的最前面。
虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符”\0”一樣,其標誌了虛函數表的結束。這個結束標誌的值在不同的編譯器下是不同的。在Visual Studio下,這個值是NULL。而在linux下,如果這個值是1,表示還有下一個虛函數表,如果值是0,表示是最後一個虛函數表。
因為對象b中多了一個指向虛函數表的指針,而指針的sizeof是8,因此含有虛函數的類或實例最後的sizeof是實際的數據成員的sizeof加8。
class Base {
public: int a; virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }
sizeof(Base)為16(vptr指針的大小為8,又因為對象中還包含一個int變量,字節對齊得8+8=16)。
四、基類含有虛函數的繼承
(1)在派生類中不對基類的虛函數進行覆蓋,同時派生類中還擁有自己的虛函數,比如有如下的派生類:
class Derived: public Base { public: virtual void f1() { cout << "Derived::f1" << endl; } virtual void g1() { cout << "Derived::g1" << endl; } virtual void h1() { cout << "Derived::h1" << endl; } };
基類和派生類的關系如下:
當定義一個Derived的對象d後,其成員的存放如下:
可以發現:
1)虛函數按照其聲明順序放於表中。
2)基類的虛函數在派生類的虛函數前面。
此時基類和派生類的sizeof都是數據成員的大小+指針的大小8。
(2)在派生類中對基類的虛函數進行覆蓋,假設有如下的派生類:
class Derived: public Base { public: virtual void f() { cout << "Derived::f" << endl; } virtual void g1() { cout << "Derived::g1" << endl; } virtual void h1() { cout << "Derived::h1" << endl; } };
基類和派生類之間的關系:其中基類的虛函數f在派生類中被覆蓋了。
當我們定義一個派生類對象d後,其d的成員存放為:
可以發現:
1)覆蓋的f()函數被放到了虛表中原來基類虛函數的位置;
2)沒有被覆蓋的函數依舊;
3)派生類的大小仍是基類和派生類的非靜態數據成員的大小+一個vptr指針的大小;
Base *b = new Derive(); b->f();
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
(3)多繼承:無虛函數覆蓋
假設基類和派生類之間有如下關系:
對於派生類實例中的虛函數表,是下面這個樣子:
可以看到:
1) 每個基類都有自己的虛表;
2) 派生類的成員函數被放到了第一個基類的表中(所謂第一個基類是按照聲明順序來判斷的);
由於每個基類都需要一個指針來指向其虛函數表,因此d的sizeof等於d的數據成員加上三個指針的大小。
(4)多重繼承,含虛函數覆蓋 :
假設,基類和派生類又如下關系:派生類中覆蓋了基類的虛函數f 。
可以看見,三個基類虛函數表中的f()的位置被替換成了派生類的函數指針。這樣,就可以任一靜態類型的基類類來指向派生類,並調用派生類的f()了。
Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g()
此情況派生類的大小也是類的所有非靜態數據成員的大小+三個指針的大小。
#include<iostream> using namespace std; class A { }; class B { char ch; virtual void func0() { } }; class C { char ch1; char ch2; virtual void func() { } virtual void func1() { } }; class D: public A, public C { int d; virtual void func() { } virtual void func1() { } }; class E: public B, public C { int e; virtual void func0() { } virtual void func1() { } }; int main(void) { cout<<"A="<<sizeof(A)<<endl; //result=1 cout<<"B="<<sizeof(B)<<endl; //result=16 cout<<"C="<<sizeof(C)<<endl; //result=16 cout<<"D="<<sizeof(D)<<endl; //result=16 cout<<"E="<<sizeof(E)<<endl; //result=32 return 0; }
結果分析:
1.A為空類,所以大小為1 ;
2.B的大小為char數據成員大小+vptr指針大小。由於字節對齊,大小為8+8=16 ;
3.C的大小為兩個char數據成員大小+vptr指針大小。由於字節對齊,大小為8+8=16 ;
4.D為多繼承派生類,由於D有數據成員,所以繼承空類A時,空類A的大小1字節並沒有計入當中,D繼承C,此情況D只需要一個vptr指針,所以大小為數據成員加一個指針大小。由於字節對齊,大小為8+8=16 ;
5.E為多繼承派生類,此情況為我們上面所講的多重繼承,含虛函數覆蓋的情況。此時大小計算為數據成員的大小+2個基類虛函數表指針大小 ,考慮字節對齊,結果為8+8+2*8=32;
四.虛繼承的情況
對虛繼承層次的對象的內存布局,在不同編譯器實現有所區別。
在這裏,只說一下在gcc編譯器下,虛繼承大小的計算。它在gcc下實現比較簡單,不管是否虛繼承,GCC都是將虛表指針在整個繼承關系中共享的,不共享的是指向虛基類的指針。
class A { int a; }; class B:virtual public A{ virtual void myfunB(){} }; class C:virtual public A{ virtual void myfunC(){} }; class D:public B,public C{ virtual void myfunD(){} };
sizeof(A)=16,sizeof(B)=24,sizeof(C)=24,sizeof(D)=32.
(A的大小為int大小加上虛表指針大小。B,C中由於是虛繼承因此大小為int大小加指向虛基類的指針的大小。B,C雖然加入了自己的虛函數,但是虛表指針是和基類共享的,因此不會有自己的虛表指針,他們兩個共用虛基類A的虛表指針。D由於B,C都是虛繼承,因此D只包含一個A的副本,於是D大小就等於int變量的大小+B中的指向虛基類的指針+C中的指向虛基類的指針+一個虛表指針的大小,由於字節對齊,結果為8+8+8+8=32)
C++類的大小計算匯總