天天看點

深度探索C++對象模型

一、關于對象

C++在布局以及存取時間上主要的額外負擔是由virtual引起的

virtual function :支援一個有效率的執行期綁定,多态。

virtual base class :實作多次出現在繼承體系中的base class,有一個單一而被共享的執行個體

1.1 C++對象模型

加上封裝後的布局成本

在C++中,有兩種class data members:static和nonstatic;三種class member functions:static、nonstatic和virtual。

C++對象模型

在此模型中,Nonstatic data members被配置于每一個class object之内,static data members則被存放在class object之外。static和nonstatic function members也被放在class object之外。

Virtual functions則以兩個步驟支援之:

每一個 class産生一堆虛函數指針,放在表格之中。這個表格被稱為虛函數表(vtbl)。

每一個class object安插一個指針,指向相關的virtual table。通常這個指針被稱為虛表指針(vptr)。vptr的設定(setting)和重置(resetting)都由每一個class的constructor、destructor和copy assignment運算符自動完成。

1.2 關鍵詞所帶來的差異

如果一個程式員迫切需要一個相當複雜的C++ class的某部分資料,使他擁有C聲明的那種模樣,那麼那一部分最好抽取出來成為一個獨立的struct聲明。C struct在C++中的一個合理用途,是當你要傳遞“一個複雜的class object的全部或部分”到某個C函數去時,struct聲明可以将資料封裝起來,并保證擁有與C相容的空間布局。

1.3 對象的差異

C++程式設計模型直接支援三種programming paradigms(程式設計範式):

程式模型(procedural model)。就像 C一樣,C++當然也支援它。

基于對象模型(abstract data type model,ADT)。此模型所謂的“抽象”是和一組表達式(public接口)一起提供的,那時其運算定義仍然隐而未明。

面向對象模型(object-oriented model)。在此模型中有一些彼此相關的類型,通過一個抽象的 base class(用以提供共同接口)被封裝起來。

多态的主要用途是經由一個共同的接口來影響類型的封裝,這個接口通常被定義在一個抽象的base class中。

需要多少記憶體才能夠表現一個class object?一般而言要有:

其 nonstatic data members的總和大小。

加上任何由于 alignment(譯注)的需求而填補(padding)上去的空間(可能存在于 members之間,也可能存在于集合體邊界)。譯注:alignment就是将數值調整到某數的倍數。在32位計算機上,通常alignment為4 bytes(32位),以使bus的“運輸量”達到最高效率。

加上為了支援 virtual而由内部産生的任何額外負擔(overhead)。

指針的類型(The Type of a Pointer)

“指針類型”會教導編譯器如何解釋某個特定位址中的記憶體内容及其大小。轉換(cast)其實是一種編譯器指令,并不改變一個指針所含的真正位址,它隻影響“被指出之記憶體的大小和其内容”的解釋方式。

總而言之,多态是一種威力強大的設計機制,允許你繼承一個抽象的public接口之後,封裝相關的類型。需要付出的代價就是額外的間接性——不論是在“記憶體的獲得”或是在“類型的決斷”上。C++通過class的pointers和references來支援多态,這種程式設計風格就稱為“面向對象”。

C++也支援具體的ADT程式風格,如今被稱為object-based(OB)。非多态的資料類型提供一個public 接口和一個private實作品,包括資料和算法,但是不支援類型的擴充。OB設計比對等的OO設計速度更快而且空間更緊湊。速度快是因為所有的函數調用操作都在編譯時期解析完成,對象建構起來時不需要設定 virtual機制;空間緊湊則是因為每一個class object 不需要負擔傳統上為了支援virtual機制而需要的額外負荷。不過,OB設計比較沒有彈性。

2.1 Default Constructor的構造操作

“帶有 Default Constructor”的 Member Class Object

如果一個class沒有任何constructor,但它内含一個member object,而後者有default constructor,那麼這個class的implicit default constructor就是“nontrivial”,編譯器需要為該class 合成出一個default constructor。不過這個合成操作隻有在constructor真正需要被調用時才會發生。再一次請你注意,被合成的default constructor隻滿足編譯器的需要,而不是程式的需要。

“帶有 Default Constructor”的 Base Class

如果一個沒有任何constructors的class派生自一個“帶有default constructor”的base class,那麼這個derived class 的default constructor 會被視為nontrivial,并是以需要被合成出來。它将調用上一層 base classes 的 default constructor(根據它們的聲明順序)。對一個後繼派生的class而言,這個合成的constructor和一個“被顯式提供的default constructor”沒有什麼差異。

“帶有一個 Virtual Function”的 Class

另有兩種情況,也需要合成出default constructor:

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

class派生自一個繼承串鍊,其中有一個或更多的 virtual base classes。

“帶有一個 Virtual Base Class”的 Class

Virtual base class 的實作法在不同的編譯器之間有極大的差異。然而,每一種實作法的共同點在于必須使virtual base class在其每一個derived class object中的位置,能夠于執行期準備妥當。

有4種情況,會造成“編譯器必須為未聲明 constructor 的classes合成一個default constructor”。C++Standard 把那些合成物稱為 implicit nontrivial default constructors。被合成出來的constructor隻能滿足編譯器(而非程式)的需要。它之是以能夠完成任務,是借着“調用member object或base class的default constructor”或是“為每一個object初始化其virtual function機制或virtual base class機制”而完成的。至于沒有存在那4種情況而又沒有聲明任何constructor的classes,我們說它們擁有的是implicit trivial default constructors,它們實際上并不會被合成出來。

在合成的 default constructor 中,隻有 base class subobjects 和 member class objects會被初始化。所有其他的nonstatic data member(如整數、整數指針、整數數組等等)都不會被初始化。這些初始化操作對程式而言或許有需要,但對編譯器則非必要。如果程式需要一個“把某指針設為0”的default constructor,那麼提供它的人應該是程式員。

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

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

編譯器合成出來的default constructor會顯式設定“class 内每一個 data member的預設值”。

2.2 Copy Constructor的構造操作

Default Memberwise Initialization

當class object 以“相同 class 的另一個 object”作為初值,其内部是以所謂的default memberwise initialization手法完成的,也就是把每一個内建的或派生的data member(例如一個指針或一個數組)的值,從某個object拷貝一份到另一個object身上。不過它并不會拷貝其中的 member class object,而是以遞歸的方式施行 memberwise initialization。

C++Standard上說,如果class沒有聲明一個copy constructor,就會有隐式的聲明(implicitly declared)或隐式的定義(implicitly defined)出現。和以前一樣,C++Standard 把copy constructor區分為trivial和nontrivial兩種。隻有nontrivial的執行個體才會被合成于程式之中。決定一個copy constructor是否為trivial的标準在于class 是否展現出所謂的“bitwise copy semantics”。

Bitwise Copy Semantics(位逐次拷貝)

在這被合成出來的copy constructor中,如整數、指針、數組等等的non class members也都會被複制,正如我們所期待的一樣。

不要 Bitwise Copy Semantics!

什麼時候一個class不展現出“bitwise copy semantics”呢?有4種情況:

當class内含一個member object而後者的class聲明有一個copy constructor時(不論是被 class設計者顯式地聲明,就像前面的 String那樣;或是被編譯器合成,像 class Word那樣)。

當class繼承自一個base class而後者存在一個copy constructor時(再次強調,不論是被顯式聲明或是被合成而得)。

當class聲明了一個或多個virtual functions時。

當class派生自一個繼承串鍊,其中有一個或多個virtual base classes時。

重新設定Virtual Table的指針

回憶編譯期間的兩個程式擴張操作(隻要有一個class聲明了一個或多個virtual functions就會如此):

增加一個virtual function table(vtbl),内含每一個有作用的virtual function的位址。

一個指向virtual function table的指針(vptr),安插在每一個class object内。

合成出來的ZooAnimal copy constructor 會顯式設定object的vptr指向ZooAnimal class的virtual table,而不是直接從右手邊的class object中将其vptr現值拷貝過來。

處理 Virtual Base Class Subobject

Virtual base class的存在需要特别處理。一個class object 如果以另一個object作為初值,而後者有一個 virtual base classsubobject,那麼也會使“bitwise copy semantics”失效。

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

我們已經看過4種情況,在那些情況下class不再保持“bitwise copy semantics”,而且 default copy constructor 如果未被聲明的話,會被視為nontrivial。在這4種情況下,如果缺乏一個已聲明的copy constructor,編譯器為了正确處理“以一個class object 作為另一個class object 的初值”,必須合成出一個copy constructor。

2.3 程式轉化語意學

轉化

顯式的初始化操作(Explicit Initialization)

參數的初始化(Argument Initialization)

傳回值的初始化(Return Value Initialization)

優化方法:

在使用者層面做優化(Optimization at the User Level)

在編譯器層面做優化(Optimization at the Compiler Level)。Named Return Value(NRV)優化

copy constructor的應用,迫使編譯器多多少少對你的程式代碼做部分轉化。尤其是當一個函數以傳值(by value)的方式傳回一個class object,而該class有一個copy constructor(不論是顯式定義出來的,或是合成的)時。這将導緻深奧的程式轉化——不論在函數的定義上還是在使用上。此外,編譯器也将copy constructor的調用操作優化,以一個額外的第一參數(數值被直接存放于其中)取代 NRV。程式員如果了解那些轉換,以及copy constructor 優化後的可能狀态,就比較能夠控制其程式的執行效率。

2.4 成員們的初始化隊伍(Member Initialization List)

當你寫下一個constructor時,就有機會設定class members的初值。要不是經由member initialization list,就是在constructor函數本體之内。

在下列情況下,為了讓你的程式能夠被順利編譯,你必須使用member initialization list:

當初始化一個reference member時;

當初始化一個const member時;

當調用一個base class的constructor,而它擁有一組參數時;

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

編譯器會一一操作initialization list,以适當順序在constructor之内安插初始化操作,并且在任何explicit user code之前。list中的項目順序是由class中的members聲明順序決定的,不是由initialization list中的排列順序決定的。

簡略地說,編譯器會對initialization list 一一處理并可能重新排序,以反映出members的聲明順序。它會安插一些代碼到constructor體内,并置于任何explicit user code之前。

Nonstatic data members放置的是“個别的class object”感興趣的資料,static data members則放置的是“整個class”感興趣的資料。

對于nonstatic data members,直接存放在每一個class object之中。對于繼承而來的nonstatic data members (不管是virtual還是nonvirtual base class)也是如此。至于static data members,則被放置在程式的一個global data segment 中,不會影響個别的class object的大小。在程式之中,不管該class被産生出多少個objects(經由直接産生或間接派生),static data members永遠隻存在一份執行個體(譯注:甚至即使該class沒有任何object執行個體,其static data members也已存在)。

3.1 Data Member的綁定

是以在一個inline member function軀體之内的一個data member綁定操作,會在整個class聲明完成之後才發生。然而,這對于member function的argument list并不為真。Argument list中的名稱還是會在它們第一次遭遇時被适當地決議(resolved)完成。

3.2 Data Member的布局

Nonstatic data members在class object中的排列順序将和其被聲明的順序一樣,任何中間介入的static data members都不會被放進對象布局之中。

編譯器還可能會合成一些内部使用的data members,以支援整個對象模型。vptr就是這樣的東西,目前所有的編譯器都把它安插在每一個“内含virtual function之class”的 object 内。一些編譯器把vptr放在一個class object的最前端。

3.3 Data Member的存取

Static Data Members

每一個static data member隻有一個執行個體,存放在程式的data segment之中。每次存取static member時,就會被内部轉化為對該唯一extern執行個體的直接存取操作。

Nonstatic Data Members

Nonstatic data members直接存放在每一個class object 之中。經由顯式的(explicit)或隐式的(implicit)class object存取它們。欲對一個nonstatic data member進行存取操作,編譯器需要把class object的起始位址加上data member的偏移位置(offset)。

3.4 “繼承”與Data Member

在C++繼承模型中,一個derived class object所表現出來的東西,是其自己的members加上其base class members的總和。至于derived class members和base class members的排列順序,則并未在C++Standard中強制指定;理論上編譯器可以自由安排之。在大部分編譯器上頭,base class members總是先出現,但屬于virtual base class的除外。

a. 沒有繼承沒有多态:

深度探索C++對象模型

b. 隻有繼承沒有多态:

深度探索C++對象模型

c. 單繼承加多态:

深度探索C++對象模型

d. 多重繼承

對一個多重派生對象,将其位址指定給第一個base class的指針,情況将和單一繼承時相同,因為二者都指向相同的起始位址。至于第二個或後繼的 base class 的位址指定操作,則需要将位址修改過:加上(介于中間的base class subobject(s)大小。

深度探索C++對象模型

e. 虛拟繼承

深度探索C++對象模型

C++支援三種類型的member functions:static、nonstatic和virtual。

4.1 Member 的各種調用方式

Nonstatic Member Functions(非靜态成員函數)

C++的設計準則之一就是:nonstatic member function至少必須和一般的nonmember function有相同的效率。

名稱的特殊處理(Name Mangling)一般而言,member的名稱前面會被加上class名稱,形成獨一無二的命名。

Virtual Member Functions(虛拟成員函數)

vptr表示由編譯器産生的指針,指向virtual table。它被安插在每一個“聲明有(或繼承自)一個或多個 virtual functions”的class object中。事實上其名稱也會被“mangled”,因為在一個複雜的class派生體系中,可能存在多個vptrs。

1是virtual table slot的索引值,關聯到 normalize()函數。

第二個ptr表示this指針。

Static Member Functions(靜态成員函數)

如果取一個static member function的位址,獲得的将是其在記憶體中的位置,也就是其位址。由于static member function沒有this指針,是以其位址的類型并不是一個“指向class member function的指針”,而是一個“nonmember函數指針”。

4.2 Virtual Member Functions(虛拟成員函數)

virtual function的一般實作模型:每一個class有一個virtual table,内含該class之中有作用的virtual function的位址,然後每個object有一個vptr,指向virtual table的所在。在C++中,多态(polymorphism)表示“以一個public base class 的指針(或reference),尋址出一個derived class object”的意思。

一個class隻會有一個virtual table。每一個table内含其對應之class object中所有active virtual functions函數執行個體的位址。這些active virtual functions包括:

這一class所定義的函數執行個體;

繼承自base class的函數執行個體;

一個pure_virtual_called()函數執行個體,它既可以扮演pure virtual function的空間保衛者角色,也可以當做執行期異常處理函數(有時候會用到)。每一個virtual function都被指派一個固定的索引值,這個索引在整個繼承體系中保持與特定的virtual function的關系。

深度探索C++對象模型

現在,如果我有這樣的式子:<code>ptr-&gt;z()</code>

我如何有足夠的知識在編譯時期設定virtual function的調用呢?

一般而言,在每次調用 <code>z()</code>時,我并不知道ptr所指對象的真正類型。然而我知道,經由 ptr可以存取到該對象的virtual table。

雖然我不知道哪一個<code>z()</code>函數執行個體會被調用,但我知道每一個<code>z()</code>函數位址都被放在slot 4中。這些資訊使得編譯器可以将該調用轉化為:<code>(*ptr-&gt;vptr[4])(ptr)</code>

多重繼承下的Virtual Functions

在多重繼承中支援virtual functions,其複雜度圍繞在第二個及後繼的base classes身上,以及“必須在執行期調整this指針”這一點。

虛拟繼承下的Virtual Functions

一般而言,class的data member應該被初始化,并且隻在constructor中或是在class的其他member functions中指定初值。其他任何操作都将破壞封裝性質,使class的維護和修改更加困難。

5.1 “無繼承”情況下的對象構造

純虛函數的存在(Presence of a Pure Virtual Function)

可以定義和調用(invoke)一個pure virtual function;不過它隻能被靜态地調用(invoked statically),不能經由虛拟機制調用。

5.2 繼承體系下的對象構造

Constructor可能内含大量的隐藏碼,因為編譯器會擴充每一個constructor,擴充程度視class T的繼承體系而定。一般而言編譯器所做的擴充操作大約如下:

記錄在member initialization list中的data members初始化操作會被放進constructor的函數本體,并以members的聲明順序為順序。

如果有一個member并沒有出現在member initialization list之中,但它有一個default constructor,那麼該default constructor必須被調用。

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

在那之前,所有上一層的base class constructors必須被調用,以base class的聲明順序為順序(與 member initialization list中的順序沒關聯)。如果base class被列于member initialization list 中,那麼任何顯式指定的參數都應該傳遞過去。如果base class沒有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那麼就調用之。如果base class是多重繼承下的第二或後繼的base class,那麼this指針必須有所調整。

在那之前,所有virtual base class constructors必須被調用,從左到右,從最深到最淺。如果class被列于member initialization list中,那麼如果有任何顯式指定的參數,都應該傳遞過去。若沒有列于list之中,而class有一個default constructor,亦應該調用之。此外,class中的每一個virtual base class subobject的偏移位置(offset)必須在執行期可被存取。如果class object是最底層(most-derived)的class,其constructors可能被調用;某些用以支援這一行為的機制必須被放進來。

虛拟繼承(Virtual Inheritance)

在此狀态中,“virtual base class constructors的被調用”有着明确的定義:隻有當一個完整的class object 被定義出來時,它才會被調用;如果object隻是某個完整object的subobject,它就不會被調用。

vptr初始化語意學(The Semantics of the vptr Initialization)

根本的解決之道是,在執行一個constructor時,必須限制一組virtual functions候選名單。是以為了控制一個class中有所作用的函數,編譯系統隻要簡單地控制住vptr的初始化和設定操作即可。在base class constructors調用操作之後,但是在程式員供應的代碼或是“member initialization list中所列的 members初始化操作”之前。

constructor的執行算法通常如下:

在derived class constructor中,“所有virtual base classes”及“上一層base class”的 constructors會被調用。

上述完成之後,對象的vptr(s)被初始化,指向相關的virtual table(s)。

如果有member initialization list的話,将在constructor體内擴充開來。這必須在vptr被設定之後才做,以免有一個virtual member function被調用。

最後,執行程式員所提供的代碼。

5.3 對象複制語意學(Object Copy Semantics)

我建議盡可能不要允許一個virtual base class的拷貝操作。我甚至提供一個比較奇怪的建議:不要在任何virtual base class中聲明資料。

5.4 對象的效能(Object Efficiency)

5.5 析構語意學(Semantics of Destruction)

如果class沒有定義destructor,那麼隻有在class内含的member object (或class自己的base class)擁有destructor的情況下,編譯器才會自動合成出一個來。否則,destructor被視為不需要,也就不需被合成(當然更不需要被調用)。

一個由程式員定義的destructor被擴充的方式類似constructors被擴充的方式,但順序相反:如果object内含一個vptr,那麼首先重設(reset)相關的virtual table。

destructor的函數本展現在被執行,也就是說vptr會在程式員的代碼執行前被重設(reset)。

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

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

如果有任何virtual base classes擁有destructor,而目前讨論的這個class是最尾端(most-derived)的class,那麼它們會以其原來的構造順序的相反順序被調用。

C++的一件困難事情:不太容易從程式源碼看出表達式的複雜度。

一般而言我們會把object盡可能放置在使用它的那個程式區段附近,這麼做可以節省非必要的對象産生操作和摧毀操作。

全局對象(Global Objects)

由于這樣的限制,下面這些munch政策就浮現出來了:

為每一個需要靜态初始化的檔案産生一個<code>_sti()</code>函數,内含必要的constructor調用操作或inline expansions。

在每一個需要靜态的記憶體釋放操作(static deallocation)的檔案中,産生一個<code>__std()</code>函數(譯注:我想std就是static deallocation的縮寫),内含必要的destructor調用操作,或是其 inline expansions。

提供一組runtime library“munch”函數:一個<code>_main()</code>函數(用以調用可執行檔案中的所有<code>__sti()</code>函數),以及一個<code>exit()</code>函數(以類似方式調用所有的<code>__std()</code>函數)。

局部靜态對象(Local Static Objects)

首先,我導入一個臨時性對象以保護mat_identity的初始化操作。第一次處理identity()時,這個臨時對象被評估為false,于是constructor會被調用,然後臨時對象被改為true。這樣就解決了構造的問題。而在相反的那一端,destructor也需要有條件地施行于mat_identity身上,但隻有在mat_identity已經被構造起來才算數。要判斷mat_identity是否被構造起來,很簡單,如果那個臨時對象為true,就表示構造好了。

6.2 new和delete運算符

運算符new的使用,看起來似乎是個單一運算。但事實上它是由兩個步驟完成的:

通過适當的new運算符函數執行個體,配置所需的記憶體

将配置得來的對象設立初值

尋找數組次元,對于delete運算符的效率帶來極大的沖擊,是以才導緻這樣的妥協:隻有在中括号出現時,編譯器才尋找數組的次元,否則它便假設隻有單獨一個objects要被删除。如果程式員沒有提供必須的中括号,那麼就隻有第一個元素會被析構。其他的元素仍然存在——雖然其相關的記憶體已經被要求歸還了。

6.3 臨時性對象(Temporary Objects)

臨時性對象在完整表達式尚未評估完全之前,不得被摧毀。也就是說某些形式的條件測試現在必須被安插進來,以決定是否要摧毀和第二算式有關的臨時對象。

參考連結 https://zhuanlan.zhihu.com/p/369495063

繼續閱讀