1. 程式人生 > >c++物件記憶體佈局模型

c++物件記憶體佈局模型

轉自:點選開啟連結

首先介紹一下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. 有一個虛擬函式繼承 
//子類2,有1個虛擬函式過載 
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) 當物件的虛擬函式被呼叫是,編譯器去查詢物件的虛擬函式表,找到該函式,然後呼叫。


關於虛繼承的一些總結

轉自:點選開啟連結

1.為什麼要引入虛擬繼承

虛擬繼承是多重繼承中特有的概念。虛擬基類是為解決多重繼承而出現的。如:類D繼承自類B1、B2,而類B1、B2都繼承自類A,因此在類D中兩次出現類A中的變數和函式。為了節省記憶體空間,可以將B1、B2對A的繼承定義為虛擬繼承,而A就成了虛擬基類。實現的程式碼如下:

class A

class B1:public virtual A;

class B2:public virtual A;

class D:public B1,public B2;

虛擬繼承在一般的應用中很少用到,所以也往往被忽視,這也主要是因為在C++中,多重繼承是不推薦的,也並不常用,而一旦離開了多重繼承,虛擬繼承就完全失去了存在的必要因為這樣只會降低效率和佔用更多的空間。

 

2.引入虛繼承和直接繼承會有什麼區別呢

由於有了間接性和共享性兩個特徵,所以決定了虛繼承體系下的物件在訪問時必然會在時間和空間上與一般情況有較大不同。

2.1時間:在通過繼承類物件訪問虛基類物件中的成員(包括資料成員和函式成員)時,都必須通過某種間接引用來完成,這樣會增加引用定址時間(就和虛擬函式一樣),其實就是調整this指標以指向虛基類物件,只不過這個調整是執行時間接完成的。

2.2空間:由於共享所以不必要在物件記憶體中儲存多份虛基類子物件的拷貝,這樣較之多繼承節省空間。虛擬繼承與普通繼承不同的是,虛擬繼承可以防止出現diamond繼承時,一個派生類中同時出現了兩個基類的子物件。也就是說,為了保證這一點,在虛擬繼承情況下,基類子物件的佈局是不同於普通繼承的。因此,它需要多出一個指向基類子物件的指標。

 

3.筆試,面試中常考的C++虛擬繼承的知識點

第一種情況:         第二種情況:          第三種情況            第四種情況:
class a           class a              class a              class a
{              {                {                 {
    virtual void func();      virtual void func();       virtual void func();        virtual void func();
};              };                  char x;              char x;
class b:public virtual a   class b :public a           };                };
{              {                class b:public virtual a      class b:public a
    virtual void foo();        virtual void foo();     {                 {
};              };                  virtual void foo();        virtual void foo();
                               };                };

如果對這四種情況分別求sizeof(a),  sizeof(b)。結果是什麼樣的呢?下面是輸出結果:(在vc6.0中執行)
第一種:4,12 
第二種:4,4
第三種:8,16
第四種:8,8

想想這是為什麼呢?(詳見上面的配圖)

因為每個存在虛擬函式的類都要有一個4位元組的指標指向自己的虛擬函式表,所以每種情況的類a所佔的位元組數應該是沒有什麼問題的,那麼類b的位元組數怎麼算呢?看“第一種”和“第三種”情況採用的是虛繼承,那麼這時候就要有這樣的一個指標vbptr,這個指標叫虛基類指標,也是四個位元組;還要包括類a的位元組數,所以類b的位元組數就求出來了。而“第二種”和“第四種”情況則不包括vbptr這個指標,這回應該木有問題了吧。具體內容可以詳見我的另一篇文章:點選開啟連結