C++類的儲存及類物件記憶體結構
本文分兩部分,前半部分講類的儲存後半部分講類的記憶體結構。
C++類的儲存
c++中最重要的就是類,那麼一個類的物件,它在記憶體中如何儲存的?它佔
記憶體中多少個位元組?
首先確定類的構成:
1,資料成員:可以是內建型別,類型別。
2,函式成員:虛擬函式,非虛擬函式
1)資料成員
內建型別對齊原則
內建型別就是常用的:char,short,long,int,float,double.
這些內建型別在類的物件中對齊方式,位元組為單位(在c 中結構體也是一樣的)
char 1
short 2
long 4
int 4
float 4
fouble 8
類型別對齊原則(c 中就是結構體對齊原則)
取類中最長的資料成員作為對齊原則。例如,類中最長為 double,那麼就是8 個位元組。
2)函式成員
函式成員是不佔用記憶體中類的物件的位元組。為什麼呢,你可以這樣理解,c++中為了相容c
也允許struct 作為類的宣告。在c 中struct 是用來宣告結構體型別的,只不過c 中的結構
體沒有函式成員。
同樣 c++中允許的函式成員,只不過是類給函式提供了一個作用域。
一個物件呼叫函式的時候,可以等價為普通函式的呼叫
例如:
class A
{
cout<<"Hello";
};
A a;
a.f();
a.f()等價於呼叫f(&a);類中的成員函式(static 成員函式除外)形參中都有個隱含的this
指標,它指向類物件本身。
當物件 a 呼叫f()的時候,它會把a 的地址傳給this 指標,所以f()就等價執行
f(const A* this)
{
cout<<"Hello";
}
所以物件中並不需要儲存函式成員。
下面舉個例子說明類物件的位元組數
eg.1:
class A
{
char c;
int i;
};
A a;
這物件a 的記憶體大小sizeof(a)=8(位元組為單位)
解釋下:
c 放在起始位置0,佔1 個位元組。
i 是int 要4 位元組對齊,所以前面要空3 位元組。它要從位置4 開始儲存,佔4,5,6,7 四
個位置。
最後類要按照他最長的資料成員對齊,就是i 也就是4 位元組對齊.因為已經佔用了8 個位元組,
8 是對齊4 的,所以不用額外增加位元組數了。最後sizeof(a)=8。
例子eg.2:
class B
{
doube d;
char c;
A a;//1 中的類型別A
};
B b;
這物件b 的記憶體大小sizeof(b)=24(位元組為單位)
解釋:
d 放在起始位置0 到7,佔8 個位元組。
c 是char 要1 位元組對齊,所以放在位置8,佔1 個位元組。
b 是類型別,在1 中可以知道它是8 位元組對齊的,所以前面要空7 個位元組,它從位置16
開始儲存,一直到23,佔8 個位元組。
最後類要按照他最長的資料成員對齊,就是d 也就是8 位元組對齊,因為已經佔用了24 個字
節,24 是對齊8 的,所以不用額外增加位元組數了。最後sizeof(a)=24。
例子eg.3:
class c
{
char c;
int i1;
double d;
int i2;
};
C c;
你知道sizeof(c)=多少嗎? 答案:首先儲存字元變數c,0位置儲存,佔1個位元組;然後儲存整型變數i1,4個位元組對齊,因此從4~7位置儲存,佔4個位元組;然後儲存雙精度變數d,從8~15位置儲存,佔8個位元組;最後儲存整型變數i2,從位置16~19儲存佔4個位元組;最後對齊到最長的(8個位元組),及補全20~23位置,整個佔24個位元組。
下面說下特殊的,就是 c 中沒有的。
【1】類中有虛擬函式的時候
我們在一開始的時候,就說了成員函式中有虛擬函式。c++為了處理多型,所以引入虛擬函式,
在一個類物件儲存空間中,第一個位置需要4 個位元組來儲存一個指標。這個指標是指向改
類的虛擬函式表的。也就是這個指標的值就是改類的虛擬函式表的地址。所以就比上面說的多了
4 個位元組。
例如:
class D
{
public:
virtual void f(){};
double d;
}
D d;
sizeof(d)=16;
【2】派生類記憶體大小
例如:
class E:D
{
int d0;
char c;
int d1;
};
E e;
sizeof(e)=32;
解釋:
基類中有虛擬函式,所以派生類物件一開始要 4 個位元組儲存指向虛擬函式表的指標。
然後繼承 D 中的資料成員double d;
它要8 位元組對齊,所以前面空4 個位元組。
下面就開始儲存 d0,c,d1.最後類對齊可計算得到32.
類物件記憶體結構
首先介紹一下C++中有繼承關係的類物件記憶體的佈局:
在C++中,如果類中有虛擬函式,那麼它就會有一個虛擬函式表的指標__vfptr,在類物件最開始的記憶體資料中。之後是類中的成員變數的記憶體資料。
對於子類,最開始的記憶體資料記錄著父類物件的拷貝(包括父類虛擬函式表指標和成員變數)。 之後是子類自己的成員變數資料。
對於子類的子類,也是同樣的原理。但是無論繼承了多少個子類,物件中始終只有一個虛擬函式表指標。
為了探討C++類物件的記憶體佈局,先來寫幾個類和函式
首先寫一個基類:
class Base
{
public:
virtual void f() { cout << “Base::f” << endl; }
virtual void g() { cout << “Base::g” << endl; }
virtual void h() { cout << “Base::h” << endl; }
int base;
protected:
private:
};
然後,我們多種不同的繼承情況來研究子類的記憶體物件結構。
(1) 無虛擬函式集繼承
//子類1,無虛擬函式過載
class Child1 : public Base
{
public:
virtual void f1() { cout << “Child1::f1” << endl; }
virtual void g1() { cout << “Child1::g1” << endl; }
virtual void h1() { cout << “Child1::h1” << endl; }
int child1;
protected:
private:
};
這個子類Child1沒有繼承任何一個基類的虛擬函式,因此它的虛擬函式表如下圖:
我們可以看出,子類的虛擬函式表中,先存放基類的虛擬函式,在存放子類自己的虛擬函式。
(2)有一個虛擬函式繼承
class Child2 : public Base
{
public:
virtual void f() { cout << “Child2::f” << endl; }
virtual void g2() { cout << “Child2::g2” << endl; }
virtual void h2() { cout << “Child2::h2” << endl; }
int child2;
protected:
private:
};
當子類重寫了父類的虛擬函式,則編譯器會將子類虛擬函式表中對應的父類的虛擬函式替換成子類的函式。
(3)全部虛擬函式都繼承
//子類3,全部虛擬函式過載
class Child3 : public Base
{
public:
virtual void f() { cout << “Child3::f” << endl; }
virtual void g() { cout << “Child3::g” << endl; }
virtual void h() { cout << “Child3::h” << endl; }
protected:
int x;
private:
};
(4)多重繼承
多重繼承,即類有多個父類,這種情況下的子類的記憶體結構和單一繼承有所不同。
我們可以看到,當子類繼承了多個父類,那麼子類的記憶體結構是這樣的:
子類的記憶體中,順序 :
(5) 菱形繼承
(6)單一虛擬繼承
虛擬繼承的子類的記憶體結構,和普通繼承完全不同。虛擬繼承的子類,有單獨的虛擬函式表, 另外也單獨儲存一份父類的虛擬函式表,兩部分之間用一個四個位元組的0x00000000來作為分界。子類的記憶體中,首先是自己的虛擬函式表,然後是子類的資料成員,然後是0x0,之後就是父類的虛擬函式表,之後是父類的資料成員。
如果子類沒有自己的虛擬函式,那麼子類就不會有虛擬函式表,但是子類資料和父類資料之間,還是需要0x0來間隔。
因此,在虛擬繼承中,子類和父類的資料,是完全間隔的,先存放子類自己的虛擬函式表和資料,中間以0x分界,最後儲存父類的虛擬函式和資料。如果子類過載了父類的虛擬函式,那麼則將子類記憶體中父類虛擬函式表的相應函式替換。
(7)菱形虛擬繼承
結論:
(1) 對於基類,如果有虛擬函式,那麼先存放虛擬函式表指標,然後存放自己的資料成員;如果沒有虛擬函式,那麼直接存放資料成員。
(2) 對於單一繼承的類物件,先存放父類的資料拷貝(包括虛擬函式表指標),然後是本類的資料。
(3) 虛擬函式表中,先存放父類的虛擬函式,再存放子類的虛擬函式
(4) 如果過載了父類的某些虛擬函式,那麼新的虛擬函式將虛擬函式表中父類的這些虛擬函式覆蓋。
(5) 對於多重繼承,先存放第一個父類的資料拷貝,在存放第二個父類的資料拷貝,一次類推,最後存放自己的資料成員。其中每一個父類拷貝都包含一個虛擬函式表指標。如果子類過載了某個父類的某個虛擬函式,那麼該將該父類虛擬函式表的函式覆蓋。另外,子類自己的虛擬函式,儲存於第一個父類的虛擬函式表後邊部分。
(6) 當物件的虛擬函式被呼叫是,編譯器去查詢物件的虛擬函式表,找到該函式,然後呼叫。
最後的三種繼承型別還沒有理解清楚,後續補充完善。