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; // Object of type NoMembers.
cout << "The size of an object of empty class is: "
<< sizeof(n) << endl;
}
輸出:
The size of an object of empty class is: 1
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形象地看成一個函數指針數組,這個數組的每個元素存放的就是虛函數的地址。在每個帶有虛函數的類 中,編譯器祕密地置入一指針,稱爲v p o i n t e r(縮寫爲V P T R),指向這個對象的V TA B L E。 當構造該派生類對象時,其成員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”一樣,其標誌了虛函數表的結束。這個結束標誌的值在不同的編譯器下是不同的。在vs下,這個值是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)沒有被覆蓋的函數依舊。
派生類的大小仍是基類和派生類的非靜態數據成員的大小+一個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。
---------------------
作者:阿阿阿阿阿阿鑫
來源:CSDN
原文:https://blog.csdn.net/fengxinlinux/article/details/72836199