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)