喵哥最近在複習C++虛函數的時候遇到一道題,關於計算類的大小的問題,爲了吃透這種題目,在網上找到幾篇文章學習了一下。也發現了不少問題,藉此契機記錄一二。
C++的類大小計算通常會加入虛函數,靜態成員,虛繼承,多繼承等情況,這些情況都對應着不同的計算方式。
在計算類佔用空間的大小時,一般先粗略的用如下規則進行判斷:
1.類大小的計算遵循結構體的對齊原則
2.類的大小與普通數據成員有關,與成員函數和靜態成員無關。即普通成員函數,靜態成員函數,靜態數據成員,
靜態常量數據成員均對類的大小無影響(靜態數據成員之所以不計算在類的對象大小內,是因爲類的靜態數據成
員被該類所有的對象所共享,並不屬於具體哪個對象,靜態數據成員定義在內存的全局區)
4.虛函數對類的大小有影響,是因爲虛函數表指針帶來的影響
5.虛繼承對類的大小有影響,是因爲虛基表指針帶來的影響
6.空類的大小是一個特殊情況,空類的大小爲1
VS與gcc的編譯結果都是在繼承的地方會出現偏差,所以在後面再具體區分gcc和VS,使用的都是x64,指針大小爲8字節。
1.字節對齊。靜態成員、普通成員函數無影響
#include<iostream>
using namespace std;
class base
{
public:
base()=default; //構造函數
~base()=default;//析構函數
void haha(int x){ b = x;}
private:
static int a; //靜態成員
int b;
char c;
};
int main()
{
base obj;
cout<<sizeof(obj)<<endl;
}
base的大小爲8。用之前的判斷方法,只有b和c是需要計算大小的,又由於字節對齊的要求,所以就是4+4 = 8。
需要注意的是這個字節對齊的方式,它是按照變量聲明的順序對齊的。比如:
int b;
char c1;
char c2;
這樣的結果也是8,因爲後面的倆char可以在一個4字節的段內。
而對於這樣的情況:
char c1;
int b;
char c2;
由於倆char分開了,總的大小變成12字節。
2.計算空類
對於一個不帶任何數據的類,生成的對象大小不會爲0,畢竟需要分配內存的,大小爲1。
在繼承空類時,這一個字節是不會被加到派生類中的。但是在一個類中聲明一個空類對象爲數據成員,那麼在計算這個類時,需要加上這一個字節。
class Empty {};
class AA{
int x;
Empty e;
};
AA的大小爲8。
3.含有虛函數的類
虛函數(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; }
};
一個沒有繼承的其他虛函數表的類只有一個虛函數表,所以其對象只有一個虛函數表指針。在x64的環境下,大小爲8.
虛函數指針在對象的最前面,所以在對齊字節的時候不用考慮虛函數聲明的位置。
#include <iostream>
using namespace std;
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; }
char c;
};
int main()
{
Base obj;
cout << sizeof(obj) << endl;
return 0;
}
這裏的對齊方式是:
所以結果爲16。
4.含有虛函數的繼承
4.1派生類不覆蓋基類的派生類,並且增加新的虛函數。
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; }
char c;
};
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的對象的虛函數表的指針指向的的結構爲:
1)虛函數按照其聲明順序放於表中。
2)基類的虛函數在派生類的虛函數前面。
此時基類和派生類的sizeof都是數據成員的大小+指針的大小8。
4.2派生類覆蓋基類的派生類的虛函數
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; }
char c;
};
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; }
};
此時Derived對象的虛函數表的指針指向的結構爲:
可以發現:
1)覆蓋的f()函數被放到了虛表中原來基類虛函數的位置。
2)沒有被覆蓋的函數依舊。
派生類的大小仍是基類和派生類的非靜態數據成員的大小+一個vptr指針的大小
所以繼承一個基類的情況下,只會增加一個虛函數表。同理,簡單的繼承多個基類的情況,就會有同樣個數的虛函數表。
4.3多繼承:無虛函數覆蓋
繼承關係:
派生類對象的虛函數表:
我們可以看到:
1) 每個基類都有自己的虛表。
2) 派生類的成員函數被放到了第一個基類的表中。(所謂的第一個基類是按照聲明順序來判斷的)
由於每個基類都需要一個指針來指向其虛函數表,因此d的sizeof等於d的數據成員加上三個指針的大小。
4.4多重繼承,含虛函數覆蓋
繼承的結構:
派生類對象的虛函數表:
#include<iostream>
using namespace std;
class A
{
/*char a;
long long aa;*/
};
class B
{
virtual void func0() { }
char ch;
char ch1;
};
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() { }
};
class F : public D, public E {
int f;
};
int main(void)
{
//測試結果再x64下運行
cout << "int = " << sizeof(int) << endl;
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=24
cout << "E=" << sizeof(E) << endl; //result=40
cout << "F=" << sizeof(F) << endl; //result=72
return 0;
}
注意:以上結果都是在VS的x64環境下運行的結果,如果在gcc下會不一樣,具體表現爲:
cout << "int = " << sizeof(int) << endl;
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
cout << "F=" << sizeof(F) << endl; //result=56
return 0;
因爲派生類會繼承基類的普通數據成員,所以需要考慮數據的對齊方式。在windows中,各個基類的數據各自對齊,派生類也自己對齊。
在linux下,最後一個基類的數據將和派生類的數據一起對齊。
用一個例子說明:
#include<iostream>
using namespace std;
class B
{
virtual void func0() { }
char ch;
};
class C
{
char ch1;
short c2;
virtual void func() { }
virtual void func1() { }
};
class E : public B, public C
{
int e;
virtual void func0() { }
virtual void func1() { }
};
int main(void)
{
//測試結果再x64下運行
cout << "E=" << sizeof(E) << endl; //Windows result=40 Linux result=32
return 0;
}
分析:
在windows下,B、C、E類的數據成員分別對齊8字節,又有兩個虛函數表的指針,所以8*5 = 40.
在Linux下,B單獨對齊8字節,C、E的數據對齊8字節,加上兩個虛函數表的指針,8*4 = 32.
並且這個例子可以說明,B、C、E三個類的數據沒有一起對齊,假設三個類一起參與字節對齊,那麼:
紅綠藍分別代表:char、short、int。完全可以對齊一個8字節空間,這樣的話,結果應該是24,但是事實上在Linux中是32.
5.虛繼承的情況
虛繼承時,會產生虛基表,有一個虛基表指針,用來指向虛基類,多重繼承虛基類就有多個虛基表指針。