天天看點

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

    本文是《Inside the C++ Object Model》第三章的讀書筆記。主要讨論C++ data member的記憶體布局。這裡的data member 包含了class有虛函數時的vptr和vtable的布局情況。

1. 開頭幾個小問題

    1.  首先回答一個問題: 一個空類,sizeof是多少?答案是1。因為編譯器會生成一個隐晦的1bytes,用于區分,當該類多個對象時,各個對象都能在記憶體配置設定唯一位址。

    2.  還有虛函數表的指針vptr,可能在類的開始,也可能在類的結尾。通常是類的結尾。(注:比較新的VC++和GCC都是在開頭。不知道是否所有的版本都是)。

    3.  關于成員變量的記憶體對齊,例如一個類隻有char a一個屬性; 但是它的大小是4(32位。64位機器是8?但是我是用GCC的sizeof仍然是1。熟悉彙編應該知道,這個位址應該不會存其他的内容了,是以說sizeof是4/8也可了解)。雖然char的大小是1。

    4.  屬性的記憶體順序和聲明順序是一緻的。不同級别(public、protected和private)屬性的排列順序是相對一緻的,就是說可能不連續,但是必須符合較晚出現的屬性存在較高的位址。

2. vptr值的不同存儲方式

     以下的圖來自http://blog.csdn.net/hherima同學。非常感謝hherima同學的圖。我将使用hherima同學的圖,加上我自身的了解來徹底鞏固并且分享給各位可愛的程式猿們。

    下圖示範單一繼承并含有虛函數情況下的資料布局。Point2d 和Point3d是繼承關系。Point2d含有虛函數,而Point3d自身沒有虛函數。

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

    注意:vptr放在類的末尾。這種方式在剛開始被很多編譯器采用,因為可以儲存base c struct的記憶體布局。

    但是到了C++2.0,開始支援虛拟繼承和抽象基類;并且由于OO的興起,某些編譯器開始把vptr放到class object的起頭處。比如微軟的第一個C++編譯器就是采用這種方法。

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

   前端存放的好處就是編譯器可以直接通路虛函數表而不需要通過offset。當然代價就是與C的struct不再相容。但是誰會從一個C struct派生出具有虛函數的C++ class呢?

    如果是前端存放,還存在一個問題:如果基類沒有虛函數,派生類有虛函數,那麼單一繼承的自然多态就會被打破。如果要将派生類轉換成基類,必須編譯器的介入。但是這種情況也比較少,是以多态就是為了繼承,誰會設計出這種繼承呢?既然這不是大多數的case,采用vptr在開頭,那麼就具有很好的意義。這種conventional實際上很利于編譯器将C++編譯到彙編,而且彙編也比較容易讀。否則,放在結尾的話,每個class的data member數量是不一樣的,是以vptr存儲的offset也不一樣。而放到頭上,那麼0号位置存的就是vptr,1号位置存的就是第一個data member,這樣不單利于編譯代碼,也便于我們閱讀反彙編的彙編代碼。

3. 資料成員(data member)的記憶體布局

    在上一小節中我們讨論了vptr的不同存放方式。編譯器需要通過設定offset來存取vptr和data member。在98頁關于對一個nonstatic data member的存取操作描述,feel confused:作者的意思是如果是直接取對象的第一個data member,那麼需要在對象的位址+1。我不是太明白。如果是存取對象的第一個成員,那麼對象的位址應該就是指向第一個成員的,它可能是vptr,也可能是第一個data member。那麼如果是彙編,那麼直接取該位址的内容,該位址的内容有可能是成員的值,也可能存的仍是位址(指針),那麼offset+1沒有意義。如果是C++的code,那麼本身不需要這麼麻煩,誰會直接将對象所在的位址進行解釋,而不是通過C++的方式?當然某些高性能程式設計可能是,但是我實在想不出有任何理由要這樣去做。

   C++語言保證“出現在派生類中的基類對象,有其完整性”,這麼做是為了在位拷貝的時候,能夠拷貝正确。一般每個成員都會獨占一個位址,意思是在32位機器上,每個資料成員至少占用4個B。當然為了記憶體對齊,比如有一下class:

class data{
  char a;
  char b;
  int c;
};
           

       那麼a和b可能會share一個位址單元,即sizeof(data) = 8;但是子類,父類的資料成員可以為了空間效率share一個位址單元嗎?

       假如Concrete1 和Concere2都有一個char的屬性,而且Concere2繼承自Concrete1。那麼如果這兩個資料成員share一個位址單元會有什麼問題?那麼我們思考一下以下的指派能符合我們的預期嗎?

Concrete1 *pc1_1, pc1_2;
Concrete2 c2;
pc1_1 = &c2;
//memory allocate for pc1_2
*pc1_2 = *pc1_1;
           

        注意,從pc1_1到pc1_2的memberwise複制(複制一個一個的member)時,pc1_1的char b就被抹掉了。那麼pc1_1就丢掉了派生類的資訊。而這個複制很顯然不是我們需要的!

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

這也是為什麼C++語言保證“出現在派生類中的基類對象,有其完整性”!

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

3. 多重繼承(Multiple Inheritance)

       對于一個多重派生對象,将其位址指定給“最左端(也就是第一個)基類的指針”,情況和單一繼承時相同,因為兩者都指向相同的起始位址。需要付出的成本隻是位址的指定操作而已,至于第二個或後繼的base class的位址指定操作,則需要進行位址修改:加上或者減去介于中間base class大小。

       下圖展示了多繼承的關系。涉及到4個類 Point2d、Point3d、Vertex和Vertex3d(p115)

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

下面展示了多重繼承的對象模型。

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

注意,多繼承的情況下,drived clas可能會有兩個或兩個以上虛函數表指針。

請看下面的表達式:

Vertex3d   v3d;
Vertex*     pv;
Point2d*   p2d;
Point3d *  p3d;
           

那麼這個操作 pv = &v3d  需要轉換内部代碼

pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d))
           

那麼如果pv是從另外一個Vertex3d的指針(比如是pv3d)拷貝過來呢?那麼需要考慮空指針的情況。

pv = pv3d
     ?(Vertex*)(((char*)&v3d) + sizeof(Point3d))   
     :0;
           

下面這兩個操作,隻需要拷貝位址就行了。

p2d = &v3d;

p3d = &v3d;

以下引自陳皓先生的名著《C++ 對象的記憶體布局(上)》中多重繼承。使用的是VC++和GCC3.4.4

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

使用圖檔表示是下面這個樣子:

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

我們可以看到:

1)  每個父類都有自己的虛表。

2)  子類的成員函數被放到了第一個父類的表中。

3)  記憶體布局中,其父類布局依次按聲明順序排列。

4)  每個父類的虛表中的f()函數都被overwrite成了子類的f()。這樣做就是為了解決不同的父類類型的指針指向同一個子類執行個體,而能夠調用到實際的函數。

4.  虛拟多繼承情況

  下圖可以表現Vertex3d 的繼承體系圖。左為多重繼承,右為虛拟多重繼承。

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

各個class的定義如下:

class Point2d{
...
protect:
  float _x, _y;
};

class Vertex: public virtual Point2d{
...
protected:
  Vertex *next;
};

class Point3d: public virtual Point2d{
...
protected:
  float _z;
};

class Vertex3d: public Vertex, public Point3d{
...
protected:
  float mumble;
};
           

  不論是 Vertex還是 Point3d都内含一個 Point2d。然而在 Vertex3d的對象布局中,我們隻需要單一一份 Point2d就好。如何使多重繼承,那麼Vertex3d對象中将有兩個Point2d,那麼對Point2d的引用可能會有歧義。是以引入虛拟繼承。然而編譯器要實作虛拟繼承,實在是困難度頗高。虛拟繼承的原則就是:讓 Vertex和 Point3d各自維護的Point2d 折疊成一個有Vertex3d維護的單一Point2d,并且還可以儲存base class 和derived class的指針之間的多台指定操作。

    如果一個class含有virtual base classsubobjects, 那麼,該對象将被分割為兩部分:一個不變局部和一個共享局部。不變局部中的資料,不管後繼如何演化,總是擁有固定的offset,是以這部分資料可以直接存取。至于共享局部(即virtual base class),這一部分的資料,其位置會因為每次的派生操作而有變化,是以他們隻能被間接存取。各家編譯器實作技術之間的差異就是間接存取的方法不同。

     如何存取class的共享局部呢?cfront編譯器會在每一個derived class中安插一個指向virtual base class的指針,這樣就可以間接存取。這樣的實作模型會有下面兩個主要缺點:

1.每一個對象必須針對其每一個virtual base class 背負一個額外的指針。

解決方法有:第一個,Microsoft編譯器引入所謂的virtual base class table。每一個class object如果有一個或多個virtual base class,就會由編譯器安插一個指針,指向virtual base class table。至于真正的virtual base class 指針,當然是被放在該表格中。

請看下面的虛拟繼承對象模型,如圖。

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

紅框内即所謂的“共享局部”,其位置會因每次派生操作而有所變化。虛拟破壞了base class 的對象完整型,虛拟繼承會在自己類中生成一個虛函數表指針。

第二個、在virtual function table 中放置virtual base class的offset(不是位址)。

C++對象模型(五):The Semantics of Data Data語義學1. 開頭幾個小問題 2. vptr值的不同存儲方式3. 資料成員(data member)的記憶體布局 3. 多重繼承(Multiple Inheritance)4.  虛拟多繼承情況

這個方法的好處是,巧妙的利用了虛函數表的結構,使得drived class 能夠節省一個指針的大小。上圖中藍色曲線是offset

2.由于虛拟繼承串鍊的加長,導緻間接存取層次的增加。例如:如果我們有三層虛拟衍化,我就需要三次間接存取(經由三個virtual base class指針)。

這個問題的解決方案有:拷貝所有的virtual base class 的指針到drived class中。這樣就解決了存取時間的問題,雖然會有空間的開銷。

參考資料:

1. http://blog.csdn.net/haoel/archive/2008/10/15/3081328.aspx

2. http://blog.csdn.net/hherima/article/details/8888539