《深入探索c++對象模型》筆記

參考https://www.iteye.com/blog/dsqiu-1669614大部分摘自書中

這本書主要講的是c++編譯器在默默的爲我們做什麼

第一章 關於對象

 

使用class封裝之後的佈局成本:

class並沒有增加成本,data members直接內含在每一個class object之中,就像C struct一樣。而member functions雖然被包含在class的聲明之內,但是不出現在Object之中。每一個non-inline function 只會產生一個函數實體。至於inline function則會在每一個調用使用的地方產生一個函數實體(在調用點展開函數體)。

class在佈局以及存取時間上主要的額外負擔是由virtual 引起,包括:

virtual function 機制 用以支持一個有效率的“執行期綁定(runtime binding)”。

virtual base class 用以實現“多次出現在繼承體系中的 base class ,有一個單一的被共享的實體”。

當然還有一些剁成繼承下的額外負擔,發生在“一個 derived class 和其第二或後繼之 base class 的轉換”之間。

 

C++ 對象模型

在C++對象模型中,nonstatic data members 被放置在每一個class object之內,static data members 則被存放在所以class object 之外。static和nonstaitc function也被放在所有 class object 之外。virtual functions 則以兩個步驟支持之:

1.每一個 class 產生出一堆指向 virtual functions 的指針,放在表格之中,這個表格被稱爲 virtual table(vtbl).

2.每一個 class object被添加一個指針,指向相關的virtual table 。通常這個指針被稱爲 vptr ,vptr的設定和重置都由每一個 class 的 constructor 、destructor 和 copy assignment 運算符自動完成。

 

C++ 以下列方法支持多態:

1.經由一組隱含的轉化操作。例如把一個 derived class 指針轉換爲一個指向其 public base type的指針。

2.經由 virtual function 機制。

3.經由dynamic_cast和typied運算符。

 

class object 需要多少內存:

1.其 nonstatic data members的總和大小。

2.加上任何猶豫alignment的需求和padding(填補)上去的空間。

3.加上爲了支持 virtual 而由內部產生的任何額外負擔。

 

指針的類型:不同類型的指針之間的差異,既不是指針的表示方法不同,也不是指向的地址的內容不同,而是其所尋址出來的object不同,也就是說指針類型會教導編譯器如何解釋某個特定地址中的內存內容及其大小(例如:一個string是傳統的8 bytes(包括一個 4byte的字符指針和一個用來表示字符串長度的整數)。轉型(cast)其實是一個編譯指令,大部分不會改變一個指針所含有的真正地址,它隻影響“被指出值內存的大小和其內容”的解釋方式。

第二章 構造函數語意學

2.1 Default Constructor

當編譯器需要的時候,default constructor會被合成出來,只執行編譯器所需要的任務(將members適當初始化)。

  只有這4種情況下,在沒有編寫構造函數的時候,編譯器會自動添加默認構造函數。 一般人們會說,只要沒有編寫構造函數,編譯器就會添加默認構造函數,也可以這麼認爲,但是隻有這4種情況下,編譯器會在默認構造函數中添加有用的代碼。

帶有 Default Constructor 的Member Class Object

編譯器出來的是:如果一個class A 內含一個或者一個以上 member class objects ,那麼class A 的每一個 constructor 必須調用每一個member classes 的default constructor 。編譯器會擴張已存在的constructors,在其中安插一些代碼,使得 user code在被執行之前,先調用(調用順序一member objects在class 的聲明次序一致)必要的 default constructors。

 

帶有 Default Constructor 的 Base class

編譯器會在 Member Class Object 的default constructor 被插入調用之前,調用(調用次序根據他們的聲明次序)所有 base class constructor 的default constructor 。

 

帶有一個 Virtual Function 的Class

下面兩種情況同樣需要合成default constructor:

1.class 聲明(或繼承)一個 virtual function。

2.class派生自一個繼承串鏈,其中一個或者更多的 virtual base class。

擴展(constructor)操作會在編譯期間發生:

1.一個virtual function table 會被編譯器產生出來,內放class 的virtual functions 的地址。

2.在每一個 class object 中,一個額外的pointer member(vptr)會被編譯器合成出來,內含相關的class vtbl的地址。

 

帶有一個 Virtual Base Class 的class

Virtual base class的實現法在不同編譯器之間有很大差異,然而,每一個實現的共同點在於必須使 virtual base class 在其每一個 derived class object中的位置,能夠在執行期準備妥當。對於class所定義的每一個constructor 編譯器都會安插那些“允許每一個virtual base class 的執行期存取操作”的碼。

 

總結

以上四種情況,會導致“編譯器必須爲未聲明constructor 的class 合成一個default constructor ”,這只是編譯器(而非程序)的需要。它之所以能夠完成任務,是藉着“調用member object 或base class的default constructor ”或是“爲每一個object初始化其 virtual function 機制或virtual base class 機制”完成。至於沒有存在這四種情況而又沒有生命constructor的class 實際上是不會被合成出來的。

在合成的default constructor 中,只有base class subobjects(子對象)和member class objects會被初始化。所有其他的nonstatic data member ,如整數,整數指針,整數數組等是不會被初始化的,這些初始化操作對程序是必須的,但對編譯器則並非需要的。

C++新手一般有兩個誤解:

1.任何class 如果沒有定義default constructor ,就會被合成出來一個。

2.編譯器合成出來的default constructor 會明確設定 class 內每一個data member的默認值。

 

2.2 Copy Constructor 

這節主要關注三個問題:

一。這三種情況會調用 copy constructor而不是普通構造函數

有三種情況,會以一個object的內容作爲另一class object的初值。

1.最明顯的當然是對一個object做明確的初始化操作。

2.當object被當做參數交給某個函數

3.當函數返回一個class object。

Default Memberwise Initialization

如果class 沒有提供一個 explicit copy constructor時,當class object以“相同的另一個object作爲初值是,其內部是以所謂的default memberwise initialization方式完成的。也就是把每一個內建的或派生的 data member(例如一個數組或指針)的值,從某個object拷貝一份到另一個object上,但不拷貝其具體內容。例如只拷貝指針地址,不拷貝一份新的指針指向的對象,這也就是淺拷貝,不過它並不會拷貝其中member class object,而是以遞歸的方式實行memberwise initialization。

這種遞歸的memberwise initialization是如何實現的呢?

答案就是Bitwise Copy Semantics和default copy constructor。如果class展現了Bitwise Copy Semantics,則使用bitwise copy(bitwise copy semantics編譯器生成的僞代碼是memcpy函數),否則編譯器會生成default copy constructor。

二。那什麼情況下class不展現Bitwise Copy Semantics呢?有四種情況:(這四種情況,拷貝構造函數默默地爲我們會做一些除了拷貝之外的事情)

1.當class內含有一個member class object,而這個member class 內有一個默認的copy 構造函數[不論是class設計者明確聲明,或者被編譯器合成]

2.當class 繼承自 一個base class,而base class 有copy構造函數[不論是class設計者明確聲明,或者被編譯器合成]

3.當一個類聲明瞭一個或多個virtual 函數

4.當class派生自一個繼承串鏈,其中一個或者多個virtual base class

下面我們來理解這四種情況爲什麼不能使用bitwise copy,以及編譯器生成的copy constructor都幹了些什麼。

在前2種情況下,很明顯編譯器必須將member或者base class的“ copy constructor的調用操作”安插到被合成的copy constructor中。

第3種情況下,因爲class 包含virtual function, 編譯時需要做擴張操作:

1.增加virtual function table,內含有一個有作用的virtual function的地址;

2.創建一個指向virtual function table的指針,安插在class object內。

所以,編譯器對於每一個新產生的class object的vptr都必須被正確地賦值,否則將跑去執行其他對象的function了,其後果是很嚴重的。因此,編譯器導入一個vptr到class之中時,該class 就不在展現bitwise semantics,必須合成copy Constructor並將vptr適當地初始化。

第4種情況,處理Virtual Base Class Subobject

virtual base class的存在需要特別處理。一個class object 如果以另一個 virtual base class subobject那麼也會使“bitwise copy semantics”失效。

每一個編譯器對於虛擬繼承的支持承諾,都是表示必須讓“derived class object 中的virtual base class subobject 位置”在執行期就準備妥當,維護“位置的完整性”是編譯器的責任。Bitwise copy semantics 可能會破壞這個位置,所以編譯器必須自己合成出copy constructor。

 

這也就是說,拷貝構造函數和默認構造器一樣,需要的時候會進行構建,而並非程序員不寫編譯器就幫着構建。

 

三。什麼時候需要自己寫拷貝構造函數(淺拷貝深拷貝)

默認的拷貝構造函數是淺拷貝,即遇到指針,只拷貝指針的值(地址),不拷貝指針指向的內存,內存還是同一個。這樣一個對象通過這個指針改變了內存,另一個對象也會改變。並且在普通構造函數中通過指針動態創建對象(new),在析構函數中會delete。而創建對象是通過拷貝構造函數而不是普通構造函數完成的(一中的三個情況),則只會new一次,但是delete兩次。

所以在對象有動態創建的成員的時候(指針),需要自己編寫拷貝構造函數。自己用深拷貝來做,即自己先new一個內存,然後拷貝,而不是隻拷貝一個指針。

2.4 初始化列表

下面四種情況必須使用初始化列表來初始化class 的成員:

1.當初始化一個reference member時;

2.當初始化一個const member時;

3.當調用一個base class 的 constructor ,而它擁有一組參數(其實就是自定義的構造函數)時;

4.當調用一個 member class 的 constructor,而它擁有一組參數時。

不過,初始化的順序是class members聲明次序決定的,不是由初始化列表決定的。

第三章 Data 語意學

3.2 Data Member 的佈局

nonstatic data members 在class object中的排列順序將和其聲明的順序一樣的。但C++ standard 允許編譯器將多個access sections(public、 private)之中的data members自由排列,不必在乎他們的出現在class中的聲明順序。

3.3 Data Member 的存取

每一個member 的存取許可(private public protected),以及與class的關聯,並不會導致任何空間上或執行時間上的額外負擔——不論是在個別的class objects 或是在static data member 本身。

static data members 被視爲global變量,只有一個實體,存放在程序的data segment之中,每次取static member 就會被內部轉化爲對該唯一的extern 實體的直接參考操作。若取一個static data member的地址,會得到一個數據類型的指針,而不是隻取class member的指針。

nonstatic data members 欲對一個nonstatic data member 進行存取操作,編譯器需要把class object的起始地址加上data member的偏移量(在編譯時候就可以獲知)。

 

3.4 繼承與Data Member

1.只要繼承不要多態

這種情況並不會增加空間或存儲時間上的額外負擔。這種情況base class和derived class的objects都是從相同的地址開始,其差異只在於derived object 比較大,用以容納自建的nonstatic data members,把一個derived class object指定給base class 的指針或引用,並不需要編譯器去調停或修改地址,它很滋潤的可以發生,而且提供了最佳執行效率。 

2.加上多態

這種情況會帶來空間和存取時間的額外負擔

1.導入一個和virtual table ,用來存儲它所聲明的每一個virtual functions的地址。

2.在每一個class object中導入一個vptr,提供執行期的鏈接,使每一個object能夠找到相應的virtual table。

3.加強constructor,使它能夠爲vptr設定初始值,讓它指向class 所對應的virtual table 。

4.加強destructor,使它能夠消抹“指向class 相關virtual table”的vptr。

 

多重繼承

對於一個多重派生對象,將其地址指定給“最左端(第一個)base class的指針”,情況和單一繼承時相同,因爲二者都指向了相同的起始地址,至於第二個或後面的base class 的地址指定操作,則需要將地址修改過:加上(或減去,如果是downcast)介於中間的base class subobject(s)的大小。

如果要存取第二個(或後面)的base class 中的一個data member ,不需要付出額外的成本,因爲members的位置在編譯時就固定了,因此存取member只是一個簡單的offset的運算。

 

虛擬繼承

class如果含有一個或多個virtual base class subobjects將被分割爲兩部分:一個不變局部和一個共享局部。不變局部中的數據,總是能有固定的offset,這部分可以被直接存取,至於共享部分,所表現的就是virtual base class subobject ,這個部分數據,其位置因爲每次派生操作而有變化,所以只能間接存取。

如果沒有virtual functions的情況下,它們和C struct完全一樣。

第四章 Function 語意學

4.1 Member的各種調用方式

1.Nonstatic Member Functions

實際上編譯器是將member function被內化爲nonmember的形式,經過下面轉化步驟:

1.給函數添加額外參數——this。

2.將對每一個nonstaitc data member的存取操作改爲this指針來存取。

3.將member function 重寫成一個外部函數。對函數名精選mangling 處理,使之成爲獨一無二的語彙。這樣調用效率和調用一個普通的全局函數效率一樣

2.Virtual Member Functions

將ptr->f();   //f()爲virtual member function內部轉化爲(*ptr->vptr[1](ptr);

其中:

vptr表示編譯器產生的指針,指向virtual table。它被安插在每一個聲明有(或繼承自)一個或多個virtual functions 的class object 中。1 是virtual table slot的索引值,關聯到normalize()函數。第二個ptr表示this指針。

3.Static Member Functions

不能被聲明爲 const volatile 或virtual。

一個static member function 會提出於class聲明之外,並給予一個經過mangling的適當名稱。如果取一個static member function 的地址,獲得的是其在內存的位置也就是地址,而不是一個指向“class member function”的指針,如下:

&Point::count(); 

會得到一個數值,類型是:

unsigned int(*)();

而不是:

unsigned int(Point::*)();

4.2 Virtual Member Funcitons

C++中,多態表示以“一個public base class 的指針(或reference),尋址出一個derived class object”。

每一個class 只會有一個virtual table,每一個table 含有對應的class object中所有active virtual functions 函數實體地址。這些active virtual function 包括:

1.這個class 所定義的函數實體(改寫(overriding)一個可能存在的base class virtual function函數實體。

2.繼承自base class 的函數實體(不被derived class改寫)

3.一個pure_virtual_called()。

一個類繼承另一個類的時候(另一個類有虛函數),會發生三種可能性:

1.繼承base class 所聲明的virtual functions的函數實體。正確地說,是該函數實體的地址會被拷貝到derived class的virtual table相對應的slot之中。

2.使用自己的函數實體。這表示它自己的函數實體地址必須放在對應的slot之中。

3.可以加入一個新的virtual function。這時候virtual table 的尺寸會增大一個slot放進這個函數實體地址。

編譯時期設定virtual function的調用:

一般而言,我並不知道ptr 所指對象的真正類型。然而可以經由ptr 可以存取到該對象的virtual table。

雖然我不知道哪個Z()函數實體被調用,但知道每一個Z()函數地址都被放置slot 4的索引。

這樣我們就可以將

ptr->z();

轉化爲:(*ptr->vptr[4])(ptr);

唯一一個在執行期才能知道的東西是:slot4所指的到底是哪一個z()函數實體。

 

多重繼承下的 Virtual Functions

在多重繼承中支持virtual functions,其複雜度圍繞在第二個及其後面的base class 上,以及“必須在執行期調整this 指針”這一點。一般規則是,經由指向“第二或後繼base class 的指針”來調用derived class virtual function。調用操作連帶的“必要的this指針調整”操作,必須在執行期完成。

 

4.3 函數的效能

nonmemeber、static member或nonstatic member函數都被轉換爲完全相同形式,所以三者效率完全相同。

 

4.4 指向Member Function的指針

取一個nonstatic data member的地址,得到的結果是該member在class 佈局中的bytes位置,所以它需要綁定於某個class object的地址上,才能夠被存取。

取一個nonstatic member function的地址,如果該函數是nonvirtual,則得到的是內存的真正地址,然後這個值也是不完全的,也需要綁定於某個class object的地址上,才能夠調用函數。

 

支持“指向Virtual Member Function”之指針

對於一個virtual function,其地址在編譯時期是未知的,所能知道的僅是virtual function在其相關之virtual table的索引值,也就是說,對於一個virtual member function 取其地址,所能獲得的只是一個索引值。(虛函數表的索引)

 

4.5 Inline Funcitons

形參   傳入參數,直接替換 傳入常量,連替換都省了,直接變成常量 傳入函數運行結果,則需要導入臨時變量

局部變量  局部變量會被mangling,以便inline函數被替換後名字唯一 也就是說一次性調用N次,就會出現N個臨時變量……程序的體積會暴增

第五章 構造、解構、拷貝 語意學

 繼承體系下的對象構造

constructor的調用伴隨了哪些步驟:

1.初始化列表(member initialization list)的data members初始化操作會被放進constructor的函數本身,並以membs的聲明順序爲順序。

2.如果有一個member並沒有在初始化列表中,但它在一個default constructor,那麼該default constructor 必須被調用(手動)。

3.在那之前,如果class object有virtual table pointer(s),它(們)必須被設定初始值,指定適當的virtual table(s)。

4.在那之前,所有上一層的base class constructors 必須被調用,以base class 的聲明順序爲順序(與初始化列表的順序沒有關聯)。

如果base class 被列於初始化列表中,那麼任何明確指定參數都應該傳遞過去。

如果base class 沒有列於初始化列表,那麼調用default constructor。

如果base class 是多重繼承下的第二或後面的base class ,那麼this指針必須有所調整。

5.在那之前,所有 virtual base class constructors 必須被調用,從左到右,從最深到最淺。

如果class 被列於初始化列表中,那麼如果有任何明確指定的參數,都應該傳遞過去,若沒有列於初始化列表中,則調用default constructor。

此外,class中的每一個virtual base class subobject的偏移量必須在執行期可存取。

如果class object 是最底層的class,某constructors可能被調用;某些用以支持這個行爲的機制必須被放進來。

對象複製語意學

當設計一個class,並以一個class object 指定另一個class object時,有三種選擇:

1.什麼都不做,實施默認行爲。

2.提供一個explicit copy assignment operator。

3.明確拒絕一個class object指定給另一個class object。

一個class對於默認的copy assignment operator,在以下情況下不會表現出 bitwise copy語意:

1.當一個class的base class 有一個copy assignment operator時,

2.當一個class 的member object,而其class 有一個copy assignment operator時,

3.當一個class 聲明瞭任何virtual functions時,

4.當class繼承一個virtual base class 時。

 

vptr語意學

vptr在constructor何時被初始化?在base class constructors調用操作之後,但是在程序員供應的碼或是初始化列表中所列的members初始化操作之前。

 

解構語意學

destructor被擴展的方式:

1.destructor的函數本身首先被執行。

2.如果class擁有member class objects,而後擁有destructor,那麼它們會以聲明順序的相反順序被調用。

3.如果object內帶一個vptr,則現在被重新設定,指向適當的base class virtual table。

4.如果有任何直接的(上一層)nonvirtual base classes 擁有destructor ,它們會以聲明順序相反順序調用。

5.如果有任何virtual base classes 擁有destructor,而當前討論的這個class 是最尾端的class,那麼它們會以其原來順序相反順序被調用。

補充:類型向上轉型和多態的混淆

構造這樣的一個繼承體系:

class Base {

public: virtual ~Base() {}

virtual void show() { cout << "Base" << endl; }

};

class Derived : public Base {

public: void show() { cout << "Derived" << endl; }

};

子類Derived類重寫了基類Base中的show方法。 編寫下面的測試代碼:

Base b; Derived d;

b.show(); d.show();

結果是:

Base

Derived

Base的對象調用了Base的方法,而Derived的對象調用了Derived的方法。因爲直接用對象來調用成員函數時不會開啓多態機制,故編譯器直接根據b和d各自的類型就可以確定調用哪個show函數了,也就是在這兩句調用中,編譯器爲它們每一個都確定了一個唯一的入口地址。這實際上類似於一個重載多態,雖然這兩個show函數擁有不同的作用域。

那這樣呢:

Base b; Derived d;

b.show(); b = d; b.show();

現在,一個Base的對象被賦值爲子類Derived的對象。

結果是:

Base

Base

對於熟悉Java的人而言,這不可理解。但實際上,C++不是Java,它更像C。“b = d”的意思,並不是Java中的“讓一個指向Base類的引用指向它的子類對象”,而是“把Base類的子類對象中的Base子對象分割出來,賦值給b”。所以,只要b的類型始終是Base,那麼b.show()調用的永遠都是Base類中的show函數;換句話說,編譯器總是把Base中的那個show函數的入口地址作爲b.show()的入口地址。這根本就沒用上多態。

單繼承下的重寫多態

那我們再這樣:

Base b; Derived d;

Base *p = &b;

p->show();

p = &d;

p->show();

這時,結果就對了:

Base

Derived

p是一個指向基類對象的指針,第一次它指向一個Base對象,p->show()調用了Base類的show函數;而第二次它指向了一個Derived對象,p->show()調用了Derived類的show函數。

總結:也就是說,只有是指針或者引用纔是真正的多態,將子對象賦給(=)父類對象其實類型向上轉型……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章