天天看點

深度探索C++對象模型【第三章2】

1:通過對象指針還是對象來存取資料成員之間的差異:

  • 當該類是一個派生類,且其繼承鍊中有一個虛基類存在,并且存取的member成員是一個從該虛基類中繼承而來的成員時,就會有重大的差異。此時通過指針來存取的話,由于多态機制的存在,指針所綁定的對象類型要到執行期才能确定,是以存取的操作也必須延長到執行期;但是如果使用對象來存取,就不會有這樣的問題存在,成員的offset在編譯器就能夠确定(多态的執行期确定機制)。

個人了解:其實差異不大,這種情況出現的相對很少~

2:大部分編譯器對于繼承體系中,派生類中的資料排列方式,是讓基類的成員先排列的(虛基類除外)

3:一般而言,具體繼承(concrete inheritance)并不會帶來空間或者存取時間上的額外負擔。(相對于虛拟繼承來說)

4:在繼承關系下,容易犯一下的錯誤:

  • 重複設計一些相同操作的函數,且選擇某些函數稱為inline函數,是設計class的一個重要課題。
  • 為了表現class的體系抽象,将class分為很多層,進而這樣就會浪費非常多的記憶體空間(這樣造成的複制功能也會産生差異)

5:如果沒有多态的機制,那麼進行具體繼承(無虛函數機制)的對象進行複制時,編譯器會将基類對象原本的填補空間拿出來給派生類對象覆寫,這并不是我們複制想要看到的結果~

6:加上多态之後的時間和空間成本:

  • 導入一個與該類相關的vbtl(這個table的元素個數一般是虛函數的個數再加上一兩個slot用來儲存RTTI)
  • 在該類的對象中導入一個vptr,使得每一個對象都能找到vbtl
  • 加強構造函數,使其能夠為vptr設定初值
  • 加強析構函數,使其能夠抹消vptr

這樣的時間與空間成本,給我們帶來的是多态的彈性:指針或引用綁定的類型不确定

7:将vptr放在對象的哪一個位置一直随着編譯器的發展在變化。

  • 一開始是放置在類對象的末端,這樣可以保留base class C struct的對象布局,因而相容C。
  • 随着C++2.0出現,C++開始支援虛拟繼承和抽象基類,且随着OO的興起,将vptr放在對象的前端有了更大的好處,“在多重繼承下,通過類對象的指針調用虛函數”會有一定的幫助

8:在單一繼承(提供了“自然多态”)的情況下,關于繼承體系中的基類與派生類轉換。

  • 基類對象和派生類對象都是從相同的位址開始的,中間的差異隻是派生類對象比較大(算是一個部分重合的狀态)
  • 在上面的情況下,類對象從派生類到基類的轉換(通過指針或者引用),此時并不需要編譯器去改變起始位址,達到了最佳效率
  • 出現多态之後,将vptr放在了類對象的起始處,如果基類沒有虛函數而派生類有,那麼這樣的“自然多态natural ploymorphism”就會被打破。将類對象從派生類到基類的轉換(通過指針或者引用),就需要通過編譯器來調整位址(調整vptr)

9:随着多重繼承的出現,更需要編譯器的介入

  • 對一個多重派生的對象,将其位址指定為“最左端(第一個基類)base class的指針”,這樣的情況和單一繼承時相同,二者指向相同的起始位址。(額外的成本:位址的指定操作)
  • 第二個或是後續base class的位址指定操作,隻需要通過修改位址:加上對應base class對象大小即可
  • 多重繼承的存取操作并不需要付出額外的成本,因為成員的位置在編譯時已經确定,存取members隻是一個簡單的offset運算,就像單一繼承一樣~

10:随着虛拟繼承的出現,編譯器的介入也是必不可少

  • istream 和 ostream都内含有一個ios subobject。是以我們在iostream中,我們隻需要一份ios subobject即可,這就需要導入我們所謂的虛拟繼承(隻存在單一執行個體)。
  • 我們将這兩者各自維護的ios subobject折疊成一個由iostream維護的單一io subobject。
  • 一般的實作方法:一個class,如果其有一個或者多個虛基類,那麼該類将會被分割為兩部分,一個不變區域和一個共享區域。
  • 不變區域不管後續如何繼承,總是擁有固定的offset,是以這一部分是可以直接存取的。共享區域,所表現的是虛基類類對象,這一部分資料将會随着派生的進行而改變,是以他們隻能被間接存取

11:一般的政策是先建立其不變區域,再建立其共享區域。關于共享區域中資料的存取:

  • cfront編譯器的做法是:在每一個派生類對象中安插一些指針,每個指針指向一個虛基類,要存取這些繼承而來的虛基類對象中的成員,可以通過這些指針來完成。
  • cfront編譯器這樣做法的缺點有二:每個對象都必須背負對每一個虛基類所設定的一個額外指針;由于繼承串鍊的增長,導緻間接存取的層數增大,影響了存取的時間效率
  • 針對第二個缺點,現有編譯器的做法是将所有的内置虛基類指針都拷貝到派生類對象中,雖然付出了一定的空間代價,以空間換取時間
  • 針對第一個缺點,微軟編譯器提出的解決方法是生成一個虛基類表,每一個類對象如果都有一個或者多個的虛基類,就在編譯器中安插一個指針,指向虛基類表,真正的虛基類指針會被放在虛基類表中。
  • 針對第一個缺點,另一種方法是在虛函數表中放置虛基類的offset偏移量,在這種政策下,虛函數表可以經由正值或是負值來索引,正值即索引到虛函數指針,負值即所引導虛基類偏移量。

12:對于虛基類來說,有兩點值得關注的地方:

  • 虛基類帶來的時間和空間負擔以及複雜性也就說明了它的機制肯定會随着編譯器的進化而改變
  • 一般而言,虛基類最好的用法就是不包含任何的資料成員

13:編譯器的優化是非常多的,對于數組、struct/class、inline函數來說,編譯器的優化使得他們的執行速率相同,然而對于單一繼承、虛拟繼承、虛拟多重繼承來說,編譯器即使優化,也會有時間上的額外負擔。

14:class Point3d{};   Point3d origin;  

  • & Point3d::x;//取一個非靜态成員變量的位址,将會得到其在class 中的偏移量offset
  • & origin::x;//取一個綁定在真正的對象上的成員的位址,将會得到其在記憶體中的真正位址

15:指向成員的指針來存取成員,在編譯器優化條件下,效率相同。但是由于虛拟繼承需要加上虛基類的偏移值,也就多了一層的間接性(妨礙了優化的有效性)。