深度探索C++物件模型
深度探索C++物件模型
- 什麼是C++物件模型:
- 語言中直接支援面向物件程式設計的部分.
- 對於各個支援的底層實現機制.
- 抽象性與實際性之間找出平衡點, 需要知識, 經驗以及許多思考.
導讀
- 這本書是C++第一套編譯器cfront的設計者所寫.
- 瞭解C++物件模型, 有助於在語言本身以及面向物件觀念兩方面層次提升.
- explicit(明確出現於C++程式程式碼).
- implicit(隱藏於程式程式碼背後).
關於物件
- 每個非內聯(non-inline)成員函式只會誕生一個函式例項. 而行內函數會在每個使用者身上產生一個函式例項.
- C++在佈局以及存取時間上的額外負擔主要由虛(virtual)引起的:
- 虛擬函式機制(virtual function)用於支援一個有效率的執行期繫結(runtime binding).
- 虛基類(virtual base class) 用以實現多次出現在繼承體系中的基類, 有一個單一而被共享的例項.
- 額外負擔, 派生類轉換.
- 在C++中, 有兩種類資料成員: 靜態(static) 和非靜態(non-static).
- 三種類成員函式: 靜態(static), 非靜態(non-static)和虛擬函式(virtual).
- 每個資料成員或成員函式都有自己的一個slot(元素, 位置, 槽) --- 針對vtbl(虛表)而言.
- 成員函式表(member function table)成為了支援虛擬函式(virtual function)的一個有效方案.
C++物件模型
- 非靜態資料成員被置於每一個類物件中, 靜態資料成員被存放在個別的類物件之外.
- 靜態和非靜態函式也被存放在個別的類物件之外.
- 虛擬函式利用虛表(vbtl)和虛表指標(vptr)設定.
- 每個類產生一堆指向虛擬函式的指標, 並放到表格之中.
- 每個類物件被安插一個指標, 指向相關的virtual table(虛表).
- vptr的設定與重置都由每一個類的構造, 析構和copy賦值運算子自動完成.
- 每個類的type_info(型別資訊)的物件也由虛表(virtual table)指出, 通常放在表格的第一個槽(slot).
- 在虛擬繼承的情況下, 基類不管在繼承串鏈中被派生多少次, 永遠只會存在一個例項.
- class不僅是一個關鍵字, 它還會引入它所支援的封裝和繼承的哲學.
- 某種意義上, 在C++中struct和class這兩個關鍵字是可以互換的.
- 基類和派生類的資料成員的佈局沒有誰先誰後的強制規定, 但使用初始化列表時, 必須保持成員變數順序的一致性.
- 組合而非繼承, 才是把C和C++結合在一起的唯一可行方法.
物件的差異性
- 三種程式設計正規化:
- 程式模型.
- 抽象資料型別(基於物件).
- 面向物件模型.
- 應該還有一個模板程式設計(正規化模型).
- 只有通過指標或引用的間接處理基類物件, 才支援面向物件程式設計所需的多型性質.
- C++中, 多型只存在與public 類體系中, nonpublic的派生行為和void*的指標的多型性, 必須由程式設計師來顯式管理.
ps->rotate(); dynamic_cast<base_class *> (derived_class *);
- 多型的主要用途是經由一個共同的介面來影響型別的封裝, 這個介面一般定義在一個抽象的基類中.
- 一個指標, 不管它指向那種資料型別, 其本身所需記憶體大小是固定的, 與計算機的位數一致.
- 指標型別會教導編譯器如何解釋某個特定地址中的記憶體內容及其大小.
- void*指標能夠持有一個地址, 但不能通過 它來操作所指物件, 因為不知道其覆蓋怎樣的地址空間.
- 派生類不會新新增虛表指標(vptr, 繼續使用基類的指標), 只是覆蓋的地址會有所不同.
- 型別資訊的封裝並不是維護於指標之中, 而是維護於連結(link)之中, 此連結存在於物件的虛表指標(vptr), 和vptr所指的虛表(virtual table)之間.
- 編譯器必須確保每個物件有一個或一個以上的vptr, 這些vptr的內容不會被基類物件初始化或改變.
- 一個指標或引用之所以支援多型, 是因為它們並不引發記憶體中任何與記憶體相關的記憶體委託操作, 會改變的只有他們所指記憶體的"大小和內容解釋方式"而已.
- 將派生類直接用於初始化基類物件時, 派生類物件會被切割以塞入較小的基類型別記憶體中.
- C++通過指標(pointer)和引用(reference)來支援多型.
建構函式語義
- 預設建構函式的構造操作:
- 會插入一些建構函式的程式碼.
- 編譯器為未宣告任何構造的類, 編譯器會為他們合成一個預設的建構函式.
- 被合成出來的建構函式只滿足編譯器的需要.
- 合成的預設建構函式中, 只有基類派生物件成員類物件會被初始化.
- 所有其他非靜態資料成員(如整數, 整數指標, 整數陣列等)都不會被初始化.
- copy 建構函式的構造操作:
- 預設成員初始化列表, 類似於深拷貝(bitwise copy).
- 預設建構函式和預設copy建構函式在必要時才由編譯器產生出來.
- 一個類物件可以通過兩種方式複製得到, 一種是被初始化(copy constructor), 另一種是被指定(copy assignment operator).
- 位逐次拷貝(bitwise copy semantics(語義)):
- 會拷貝每一個位(bit).
- 什麼時候不要位逐次拷貝:
- 當類內含一個成員物件, 該成員物件中聲明瞭一個copy 建構函式.
- 類繼承的基類中存在一個建構函式.
- 類聲明瞭一個或多個虛擬函式.
- 當類派生自一個繼承串連, 其中有一個或多個虛基類時.
- 當編譯器匯入一個虛表指標(vptr)到一個類物件中時, 該類就不展現逐次語義(bitwise semantics)了.
程式轉換語義(Program Transformation Semantics)
- 顯示的初始化操作(Explicit Initialization):
- 程式轉換有兩個階段:
- 重寫一個定義, 其中的初始化操作會被剝離.
- 類的copy 構造呼叫操作會被安插進去.
- 程式轉換有兩個階段:
- 編譯器可能做NRV(Named Return Value)優化操作.
- 以一個類物件作為另一個類物件的初值的情形, C++允許編譯器有大量的自由發揮空間, 以提升程式效率.
- 必須使用成員初始化列表(member initialization list):
- 當初始化一個成員引用(reference member)時.
- 當初始化一個常量成員(const member)時.
- 當呼叫一個基類的建構函式, 而該基類擁有一組引數時.
- 當呼叫一個成員類的建構函式, 其擁有一組引數時.
- 編譯器會一一操作初始化列表(initialization list), 以適當順序在建構函式之內安插初始化操作, 在顯式之前.
- 初始化列表中的順序是由類的成員宣告順序決定的, 不是由初始化列表中的排列順序決定的.
- 順序混亂會造成意想不到的危險.
- 初始化列表中的專案被放在顯示宣告程式碼(explicit user code)之前.
Data語義
- 一個空類會被編譯器安插一個char, 使這個類的兩個物件得以在記憶體中配置獨一無二的地址.
- 空虛基類(Empty virtual base class)已經稱為C++面向物件的一個特有術語.
- 提供了一個虛擬介面, 沒有任何資料, 空虛基類被認為是派生物件開頭的一部分, 不花費任何派生類的額外空間.
- 虛基類自讀愛香只會在派生類中存在一份例項, 不管它在class繼承體系中出現了多少次.
- 非靜態成員資料放置的是個別類物件感興趣的資料, 靜態成員資料放置的是整個類感興趣的資料.
- 靜態成員變數被放到全域性資料段中, 不會影響個別類物件的大小. 不管生成多少個物件, 靜態資料成員永遠只存在一份例項.
- 編譯器自動加上額外的資料成員, 用以支援某些語言特性.
- 因為記憶體對齊(alignment), 邊界調整的需要. --- 類物件可能比想象的大.
資料成員的佈局
- 成員變數的排列順序因編譯器而異, 編譯器可以隨意選一個放在第一個.
- 在C++中, 在同一access section(private, protected, public等區段)中, 成員的排列只需符合較晚出現的成員變數在類物件中有較高的地址.
- 靜態成員並不需要通過類物件進行訪問.
- 一個靜態資料成員的地址是一個指向其資料型別的指標, 並不是一個指向類成員的指標.
- 對一個非靜態資料成員進行存取操作, 編譯器需要把類物件的起始地址加上資料成員的偏移位置(offset).
- 每個非靜態資料成員的偏移位置(offset)在編譯時期即可知曉.
- 具體繼承(concrete inheritance)並不會增加空間和存取時間上的額外負擔.
- 在每一個類物件(class object)中帶入一個vptr, 提供執行期的連結, 使每一個object(物件)能夠找到對應的虛表(虛virtual table).
-
在派生類和基類中, 可能重新設定vptr的值.
在解構函式中, 可能抹消掉指向類相關虛表(virtual table)的vptr.
-
- vptr放在類物件的前端(起始處), 會喪失對C語言的相容性.
- 多重繼承的問題主要發生於派生類物件和其第二或後繼的基類物件之間的轉換.
- 取一個非靜態資料成員的地址, 將得到它在類中的偏移量(offset); 取一個綁定於真正類物件身上的資料成員的地址, 將會得到該成員在記憶體中的真正地址.
函式(Function)語義
- 靜態成員函式:
- 不能直接存取非靜態成員資料.
- 也不能被宣告為const函式.
- 一般而言, 成員的名稱前面會被加上類名稱, 以形成獨一無二的命名.
- 靜態成員函式沒有this指標.
- 在C++中, 多型(polymorphsim)表示以一個public base class(公有基類)的指標(或引用), 定址一個派生類物件.
- 多型機能主要扮演一個傳送機制的角色, 可以在程式任何地方採用一組public derived型別.
- 有了RTTI(runtime tyoe identification)就能夠在執行期查詢一個多型的指標或多型的引用.
- 虛擬繼承是C++中多重繼承中特有的概念,虛擬繼承的一些總結.
-
內聯(inline)函式中的區域性變數, 再加上有副作用的引數, 可能會導致大量臨時性的物件產生.
構造, 析構, 拷貝語義
- 繼承體系中每一個類物件的解構函式都會被呼叫.
- 建構函式可能內含大量的隱藏diamante, 因為編譯器會擴充每一個constructor, 擴充成都視class 的繼承體系而定.
- 記錄在成員初始化列表中的資料成員初始化操作會被放進建構函式本體, 並以成員變數宣告順序為順序.
- 如果有一個成員變數沒有出現在初始化列表中, 它有一個預設的建構函式, 那麼該預設的建構函式必須被呼叫.
- 類物件的虛表指標(virtual table pointer)必須被設定初值, 指向適當的虛表(virtual table).
- 基類的建構函式必須被呼叫, 以基類的宣告順序為順序.
- 虛基類建構函式必須被呼叫, 從左到右, 從最深到最淺.
- 如果類沒有定義解構函式, 只有在類內的成員物件(基類)擁有解構函式時, 編譯器才會自動合成一個出來.
執行期語意
- C++所有的全域性物件都被繁殖在程式的資料段(data segment)中.
- 運算子new一般由兩個步驟完成:
- 通過適當的new運算子函式例項, 配置所需的記憶體.
- 將配置來的物件設立初值.
- 臨時物件的摧毀應該是對完整表示式(full-expression)求值過程中的最後一個步驟.
- 完整表示式(full-expression)是表示式最外圍的那個.
- 編譯器不能消除class型別的區域性臨時變數, 因為C++back-ends的限制.
- 可以通過一些優化工具把臨時物件放進暫存器.
站在物件模型的尖端
- 模板template, 異常處理exception handing(EH), 執行時型別識別(runtime type identification, RTTI).
- 每一個可執行檔案中只需要一份模板的例項, 每個編譯單位都會擁有一份例項.
- 只有在成員函式被使用的時候, C++標準才要求他們被例項化.
- 空間和時間效率的考慮.
- 尚未實現的機能.
- 所有與型別相關的檢驗, 如果牽涉到template引數, 都必須延遲到真正的例項化操作(instantiation)發生, 才得為之.
- Template中的名稱決議法 :
- 定義模板(template)的程式端和例項化模板(template)的程式的區別.
- 定義模板(template)專注於一般的模板類.
- 例項化模板(template)專注於特定的例項.
- 如果一個虛擬函式被例項化, 其例項化點緊跟在其類的例項化點之後.
- dynamic_cast運算子可以再執行期決定真正的型別.
- typeud運算子傳回一個const reference, 型別為type_info.
- 雖然RTTI只適用於多型類(polymorphic classes), 事實上type_info物件也適用於內建型別, 以及非多型的使用者自定義型別.
- 動態共享函式庫, 共享記憶體.