[基礎知識]2.關於size of(空類)的三個問題及其擴充套件
1.sizeof(沒有任何成員變數和成員函式的空類)是幾,為什麼?
- 是1B。空型別的例項中不包含任何資訊,本來求sizeof應該是0,但是當我們宣告該型別的例項的時候,它必須在記憶體中佔有一定的空間,否則無法使用這些例項。至於佔用多少記憶體,由編譯器決定。例如:在Code::Blocks和Visual Studio中每個空型別的例項佔1B。
注意:一旦類中有其他的佔用空間成員,則這1個位元組就不在計算之內。
2.sizeof(沒有任何成員變數但有一個建構函式和解構函式的類)是幾,為什麼?
- 是1B。呼叫建構函式和解構函式只需要直到函式的地址即可,而這些函式的地址只與型別相關,而與型別的例項無關,編譯器也不會因為這兩個函式而在例項內新增任何額外的資訊。
注意:如果有其他成員函式(非虛擬函式),則還是隻佔用1個位元組。
3.sizeof(沒有任何成員變數但有一個建構函式和虛解構函式的類)是幾,為什麼?
- 是4B。C++的編譯器一旦發現一個型別中有虛擬函式,就會為該型別生成虛擬函式表,並在該型別的每一個例項中新增一個指向虛擬函式表的指標。在32位的機器上,一個指標佔4位元組的空間,如果在64位的機器上,一個指標佔8位元組的空間。
注意:虛擬函式表是C++實現多型的一種機制。
總結1:
- C++標準規定類的大小不為0,空類的大小為1,當類不包含虛擬函式和非靜態資料成員時,其物件大小也為1。
- 如果在類中聲明瞭虛擬函式(不管是1個還是多個),那麼在例項化物件時,編譯器會自動在物件裡安插一個指標指向虛擬函式表VTable;在32位機器上,一個物件會增加4個位元組來儲存此指標,它是實現面向物件中多型的關鍵。
- 虛擬函式本身和其他成員函式一樣,是不佔用物件的空間的。虛擬函式本身和其他成員函式一樣,是不佔用物件的空間的。
相關知識點:
- 由於類只是一個型別定義,沒有大小可言,因此, 用sizeof運算子對一個型別名操作時,得到的實際上是該型別例項的大小。
- 一個C++的空類,即使沒有任何成員變數,編譯器也會自動生成預設建構函式、預設拷貝建構函式、預設解構函式、預設賦值函式、預設取值函式。
- 建構函式是一種特殊的成員函式, 主要用於為物件分配空間,進行初始化。
- 建構函式沒有返回值(不能說明為void型別),可以被過載,不能為虛擬函式。
- 預設建構函式不帶任何引數,函式體是空的,它只能為物件開闢資料成員儲存空間,而不能給物件中的資料成員賦初值。
- 拷貝建構函式是一種特殊的建構函式,其形參是本類物件的引用。用於在建立一個新物件時,使用一個已經存在的物件去初始化這個新物件。
- 預設拷貝建構函式用於複製出資料成員值完全相同的新物件。
- 解構函式是一種特殊的成員函式,它執行與建構函式相反的操作,通常用於執行一些清理任務,如釋放分配給物件的記憶體空間等。
- 解構函式沒有返回值(不能說明為void型別),不能被過載,可以為虛擬函式。
- 採用預設賦值函式實現的資料成員逐域賦值的方法是一種淺層複製方法。通常,預設賦值函式是能夠勝任工作的。但是,對於類似指標懸掛的問題來說,還需要使用者根據實際自己對賦值運算子進行過載(進行深層複製)。
- 淺拷貝:源物件的指標和拷貝物件的指標都指向同一個空間;
深拷貝:源物件的指標指向一個空間,拷貝物件的指標指向另一個新的空間(兩個空間的資料成員相同)。 - 虛擬函式允許函式呼叫與函式體之間的聯絡在執行時才建立,也就是在執行時才決定如何動作,即所謂的動態聯編。
- 虛擬函式的作用是允許在派生類中定義與基類同名的函式,並且可以通過基類指標或引用來訪問基類和派生類中的同名函式。其定義是在基類中進行的。
擴充套件:
下列sizeof()的大小分別是?
class Base{}; // sizeof(Base)?
class Derived:public Base{
private:
int a;
} // sizeof(Derived)?
sizeof(Base) = 1;
sizeof(Derived) = 4;
- 在空基類被繼承後,子類會優化掉空基類的1位元組大小,從而節省空間大小,提高執行效率。
class Base1{
private:
char a;
int b;
char c;
}; // sizeof(Base1)?
class Base2{
private:
char a;
char b;
int c;
}; // sizeof(Base2)?
sizeof(Base1) = 12;
- 首先,char a從0偏移開始儲存,佔一個位元組,即佔用0空間,現在可用偏移為1偏移;
接下來存int b,由於1不是四的倍數,所以向後偏移2、3,都不是四的倍數,偏移到4時,4是四的倍數,所以,b從4空間開始儲存,佔四個位元組,即佔用4、5、6、7空間,現在可用偏移為8偏移;
然後存char c,由於8是一的倍數,所以,c從8空間開始儲存,佔一個位元組,即佔用8空間,現在可用偏移為9偏移;
最後,由於9不是型別最大位元組數四的倍數,所以向後偏移10、11,都不是四的倍數,偏移到12時,12是四的倍數,因此,該類大小為12個位元組。(如圖1所示)
圖1【sizeof(Base1)】:
sizeof(Base2) = 8;
- 首先,char a從0偏移開始儲存,佔一個位元組,即佔用0空間,現在可用偏移為1偏移;
接下來存char b,由於1是一的倍數,所以,b從1空間開始儲存,佔一個位元組,即佔用1空間,現在可用偏移為2偏移;
然後存int c,由於2不是四的倍數,所以向後偏移,3不是四的倍數,偏移到4時,4是四的倍數,所以,c從4空間開始儲存,佔四個位元組,即佔用4、5、6、7空間,現在可用偏移為8偏移;
最後,由於8是型別最大位元組數四的倍數,因此,該類大小為8個位元組。(如圖2所示)
圖2【sizeof(Base2)】:
class Base{
private:
char a;
public:
virtual void f();
virtual void g();
}; // sizeof(Base)?
class Derived1:public Base{
private:
int b;
public:
void f();
}; // sizeof(Derived1)?
class Derived2:public Base{
private:
int c;
public:
void g();
virtual void h();
}; // sizeof(Derived2)?
sizeof(Base) = 8;
- 只要含虛擬函式,一定有虛擬函式表指標(vptr),而且該指標一定位於類記憶體模型最前端。
首先,vptr從0偏移開始儲存,佔四個位元組,即佔用0、1、2、3空間,現在可用偏移為4偏移;
接下來存char a,由於4是一的倍數,所以,a從4空間開始儲存,佔一個位元組,即佔用4空間,現在可用偏移為5偏移;
最後,由於5不是型別最大位元組數四的倍數,所以向後偏移6、7,都不是四的倍數,偏移到8時,8是四的倍數,因此,該類大小為8個位元組。(如圖3所示)
圖3【sizeof(Base)】:
sizeof(Derived1) = 12;
- 雖然vtbl中的Base::f()已經被替換為Derived1::f(),但是vptr並沒有改變。
首先,vptr從0偏移開始儲存,佔四個位元組,即佔用0、1、2、3空間,現在可用偏移為4偏移;
接下來存基類成員char a,由於4是一的倍數,所以,a從4空間開始儲存,佔一個位元組,即佔用4空間,現在可用偏移為5偏移;
然後存int b,由於5不是四的倍數,所以向後偏移6、7,都不是四的倍數,偏移到8時,8是四的倍數,所以,b從8空間開始儲存,佔四個位元組,即佔用8、9、10、11空間,現在可用偏移為12偏移;
最後,由於12是型別最大位元組數四的倍數,因此,該類大小為12個位元組。(如圖4所示)
圖4【sizeof(Derived1)】:
sizeof(Derived2) = 12;
- 雖然vtbl中的Base::g()已經被替換為Derived2::g(),並且新添加了虛擬函式h(),但是vptr並沒有改變。
首先,vptr從0偏移開始儲存,佔四個位元組,即佔用0、1、2、3空間,現在可用偏移為4偏移;
接下來存基類成員char a,由於4是一的倍數,所以,a從4空間開始儲存,佔一個位元組,即佔用4空間,現在可用偏移為5偏移;
然後存int c,由於5不是四的倍數,所以向後偏移6、7,都不是四的倍數,偏移到8時,8是四的倍數,所以,c從8空間開始儲存,佔四個位元組,即佔用8、9、10、11空間,現在可用偏移為12偏移;
最後,由於12是型別最大位元組數四的倍數,因此,該類大小為12個位元組。(如圖5所示)
圖5【sizeof(Derived2)】:
class Base1{
private:
char a;
public:
virtual void f();
virtual void x();
}; // sizeof(Base1)?
class Base2{
private:
int b;
public:
virtual void f();
virtual void y();
}; // sizeof(Base2)?
class Base3{
private:
double c;
public:
virtual void f();
virtual void z();
}; // sizeof(Base3)?
class Derived:public Base1, public Base2, public Base3{
private:
double d;
public:
void f();
virtual void derived_func();
}; // sizeof(Derived)?
sizeof(Base1) = 8;
sizeof(Base2) = 8;
sizeof(Base3) = 16;
sizeof(Derived) = 40;
- 由於Derived類的虛擬函式表指標與宣告繼承順序的第一個基類Base1的虛擬函式表指標合併,所以,
vtbl1中的Base1::f()被替換為Derived::f(),並且添加了新的虛擬函式derived_func();
vtbl2中的Base2::f()被替換為Derived::f();
vtbl3中的Base3::f()被替換為Derived::f();
而Derived中新新增的成員變數位於類的最後面。(如圖6所示)
圖6【sizeof(Derived)】:
class Base{
public:
int a;
virtual void f();
}; // sizeof(Base)?
class Base1:virtual public Base{
public:
int b;
virtual void x();
}; // sizeof(Base1)?
class Base2:virtual public Base{
public:
int c;
virtual void y();
}; // sizeof(Base2)?
class Derived:public Base1,public Base2{
private:
double d;
void f();
virtual void z();
}; // sizeof(Derived)?
注意:不同編譯器下虛繼承對類大小的影響是不同的!
在vs環境下,採用虛擬繼承的繼承類會有自己的虛擬函式表指標(假如基類有虛擬函式,並且繼承類添加了自己新的虛擬函式)
在gcc環境下及mac下使用clion,採用虛擬繼承的繼承類沒有自己的虛擬函式表指標(假如基類有虛擬函式,無論新增自己新的虛擬函式與否),而是共用父類的虛擬函式表指標
詳情請看:虛擬繼承對類大小的影響
這裡以32位GCC環境為例:
sizeof(Base) = 8;
sizeof(Base1) = 16;
sizeof(Base2) = 16;
- 虛繼承條件下,基類記憶體位於類的最後。
虛擬繼承會給繼承類新增一個虛基類指標(virtual base ptr 簡稱vbptr),其位於類虛擬函式指標後面,成員變數前面;
若基類沒有虛擬函式,則vbptr其位於繼承類的最前端。(如圖7所示)
圖7【sizeof(Base1)】:
sizeof(Derived) = 32;
- 記憶體順序:
Base1的虛擬函式表指標(GCC無|VS有)->Base1的虛基類指標->Base1的成員變數->Base2的虛擬函式表指標(GCC無|VS有)->Base2的虛基類指標->Base2的成員變數->Derived的成員變數->Base的虛擬函式表指標->Base的成員變數。(如圖8所示)
圖8【sizeof(Derived)】:
總結2:
- 為了優化存取效率,需要進行邊緣調整(對齊)。
- 類的大小等於類的非靜態成員資料型別的大小之和。
- 類的大小隻與它當中的成員資料有關,與類中的建構函式、解構函式以及普通成員函式無關(虛擬函式除外)。
- 前面的地址必須是後面地址的整數倍,不是就補齊。
- 整個類的地址必須是最大位元組的整數倍。
- 每個含有虛擬函式的類在記憶體中都會多一根指標(vptr),它儲存的是虛擬函式表(vtbl)所在的位置。
- 虛擬函式表(vtbl)儲存著所有虛擬函式的位置,由於其動態繫結特性,在覆寫(override)後在子類中儲存的虛擬函式位置與父類中不相同。
- 虛繼承主要是為了解決菱形繼承下公共基類的多份拷貝問題(即二義性問題和重複繼承下的空間浪費問題)。
參考文章
sizeof(空類)問題總結
sizeof() 類大小,空類大小
類可以沒有建構函式和解構函式嗎
C++類大小詳盡講解
結構體深度剖析(記憶體對齊,對齊引數,偏移量)
C++物件記憶體模型2 (虛擬函式,虛指標,虛擬函式表)