讀 深度探索C 物件模型 上

2021-09-20 17:57:09 字數 3778 閱讀 4150

【書籍資訊】

深度探索c++物件模型【inside the c++ object model】 侯捷【lippman】 華中科技大學出版社:2001

【總體概況】

本書主要是描述編譯器(和鏈結器)對c++物件模型的處理。詳述了物件導向中繼承、封裝、多型等等重要內容在編譯階段的處理。分析了各種實現的優缺點,並且展示了如何使用「分析-實現-分析...」(個人定義)這種以實踐而不是主觀臆斷為基礎的研究手段。很多深入而細緻的分析是我們從別的書中看不到的(也可能是我太孤陋寡聞了),有些具體的內容可能會有些過時(畢竟這本書寫了有一陣了),但是它包含的架構設計方法和分析問題的手段會令人有終身受益的感覺。

本書的作者和譯者都可謂是大牌了,寫作人有足夠的經驗,譯者有足夠的細心和能力,但好像這本書的影響力不是很大。也許是大多數人覺得此書描述的內容在平時程式設計中無法用到。但個人感覺本書描述的內容和思想,為我們寫出健壯和高效的**打下了基礎。建議每個有c++物件導向程式設計經驗(甚至是別的語言開發)的人閱讀一下。

【引言】

我打算從基本的c++物件模型開始,首先介紹c++物件模型對物件導向中很多新的元素的處理。比如成員函式、資料、構造和析構函式等等。為了有逐步深入的效果,這部分內容通常不涉及虛繼承。關於虛繼承的內容,會放到本文的最後來說。

筆記中加入了很多個人的理解,如果有錯誤,請指正。謝謝。

【c++物件模型】

所有的所謂的物件導向,都是在程式語言一級的。對於編譯器而言,它會將所有物件導向的內容處理成和面向過程的程式一樣。

考慮下面這個類,猜想一下編譯器是如何把這些物件導向的內容翻譯過來的。

class point 

;

書中,描述了三種方案。一是簡單物件模型,它會在非配的空間上依次儲存物件的所有資料和函式入口位址;二是**驅動模式,物件中保留資料和指向乙個**的指標,**中存放函式入口資訊。第一種方法邏輯簡單,但時間和空間(個人感覺特別是空間,有100個同類的物件就需要重複儲存100次所有函式位址)複雜度都比較高;第二種方法空間利用和靈活的都很好,但訪問函式的時間開銷增加(加了一層)。所以在很多編譯器中,都採用了第三種折衷的方案,就是所謂的c++物件模型。

如下圖所示,編譯器區別對待資料,普通函式,虛函式,靜態函式等元素,在時間、空間、靈活性中尋求乙個平衡點。書中所有的後續內容都是基於該模型的,而我們有必要了解編譯器在建立這個模型中做的很多事情,有利於我們寫出更好的**。

【資料處理】

編譯器在處理c++物件中的資料時,考慮了與c的相容性和訪問速度。通常乙個非靜態資料被放在物件空間的開始,而靜態資料被放置在乙個全域性資料段中,並保證在呼叫之前被初始化。

非靜態資料按照許可權集中放置,並保證較晚出現的放置在較高的位址。資料與資料之間通常是乙個挨乙個放置,但出於齊位的需求,在資料中間可能會被插入一些補空的資料。整個物件的大小基本等價與資料大小的總和(和齊位需求的資料)和為了保證虛函式機制引入的指標。

在一般的單繼承體系中(即不考慮虛繼承,下同),子物件的資料是挨著父物件資料存放的(在高址),而且父物件資料的存放不會被子物件影響(連用於補齊的資料也保持原樣)。上面描述的是不引入虛函式的前提下,如果引入,虛表指標通常放置在物件資料的前端(低址)或尾端(高址)。兩種方式各有其好處(放在前端有利於物件的向上轉型,而放在尾端對資料位址的處理比較簡單),其實現依具體編譯器而定。

依照上述的存放方式,考慮如下一段**:

class a 

class b : public a 

a *a = new b();

它在堆中建立了乙個b型別物件,相當於依次存放a, b, t三個資料在堆中。a指標指向b型別物件的首位址,但只能合法操作屬於它的a和b資料。可以看到這種存放機制,可以很方便的實現向上的轉型。(依照這個例子想象一下引入虛函式,虛標指標放置在前後會產生的不同問題。)

而如果引入多繼承,問題還是類似與單繼承,只是在基類資料的存放上,需要保證某個編譯器知道的順序。這樣也能可以很方便的實現向上轉型。 比如:

class a 

class b 

class c :    

public a, public b 

c *c = new c();    

a *a = c; 

b *b = c;

同樣的,堆中放置乙個c型別物件,依次包括a, b, c三個資料。在轉型成為a型別指標時,指標指向開始位址(即int a的位置),可以操作a。在轉型成為b型別指標是,指標指向double b的位置,可以操作b。

從上面的敘述可以看出:其實,封裝、繼承(普通繼承)等物件導向機制的引入,只是增加了編譯器的負擔,並不會影響到資料的訪問的效率。從書中實際測試來看,情況也確實如此,對物件中的資料操作和對非物件中的資料操作,速度基本一致。

c++物件導向模型中的函式可以視為有三類組成:一是非靜態成員函式;二是靜態成員函式;三是虛函式。

考慮型別a中有這樣乙個非靜態成員函式void test()。這個函式經過編譯器處理後,就會變成如下的格式:extern test__xxaxx(register a* const this)。也就是編譯器做了兩件事。一是為函式新增了乙個引數,該引數為該型別的乙個常指標,這樣在函式中就可以使用該型別物件的資料了;二是為函式取了個獨一無二的名字,該技術被稱為name mangling。簡單的可以認為是乙個資料方程。輸入是函式名、型別名等相關因素,輸出了乙個獨一無二的函式名。這樣當我們呼叫obj.test()的時候,就相當於在寫test_xxaxx(&obj),所謂物件導向的內容被抹乾淨了。

在非靜態成員函式中,還有一種情況就是內聯函式。眾所周知,內聯函式不算是函式,它會在所有呼叫該內聯函式處展開該函式的**(而不是乙個呼叫)。通常我們會把少量**的函式設定為inline。編譯器可以忽視你的請求(書上說會變成乙個static的函式,不解ing...),同樣編譯器也可能把乙個不是inline的函式提公升為inline。而inline帶來的好處和壞處一樣明顯。好處就是效率的提公升,壞處就是**的膨脹和臨時變數的堆積。所以使用時要仔細考慮,不要氾濫使用內聯函式(不要太依靠編譯器了)。

靜態成員函式的轉換更為簡單。只是被做了乙個簡單的name mangling。因為靜態函式並不能呼叫型別中物件的非靜態資料,所以它不需要傳入乙個物件指標。因此,靜態函式可以視為類擁有的東西,它只能夠操作屬於類的資料(靜態資料)。

虛函式的轉換從前面的那副物件模型圖中可以略見一斑。在每乙個有虛函式的物件中(不管是繼承至基類還是自己定義的)都會被安插乙個被稱為vptr的指標,該指標指向乙個被稱為vtbl的**。vtbl被分成若干個等大的slot,第乙個slot放置了關於物件型別的資訊,其他每乙個slot中都放置了乙個虛函式的入口位址。位址的具體函式可能不同,但繼承樹中同乙個(指名字和引數都相同)的虛函式會被固定放置在某個slot中。也就是如果前面所述的a中那個函式test()是虛函式,obj->test()的呼叫可能就被轉換成(*obj->vptr[1])(obj)。不難看出,這相當於乙個函式指標的呼叫。由此可知,虛函式之所以可以表現出執行期的變化,是因為它有兩個固定的內容。一是雖然物件型別不同,但它們肯定都有乙個虛指標指向虛表;二是雖然具體的函式位址不知道,但是它在虛表中的位置是固定的。

這裡所述的虛函式的實現機制,只符合單繼承模型,在多繼承和虛繼承的情況下,會有很大的不同。

最後來看下效率。書中比較了友元(相當於普通呼叫)、內聯、非靜態成員、靜態成員、單繼承虛函式、多繼承虛函式和虛擬繼承虛函式的效率。拋開多繼承不說,虛函式的引入,確實帶來了少量的效能損失(資料顯示10%左右),而除內聯外其他呼叫方式效率一致。內聯的效率出奇的好,高出了近百倍。事後作者發現,這種提公升不只是內聯本身帶來的,而是伴隨著內聯的for迴圈**調整帶來的(函式的呼叫就很難判斷了),可以看出,正確使用內聯可以出乎意料的提公升效率(很多編譯器的優化手段都可以使用了)。

柔性陣列 讀《深度探索C 物件模型》有感

最近在看 深度探索c 物件模型 對於struct的用法中,發現有一些地方值得我們借鑑的地方,特此和大家分享一下,此間內容包含了網上蒐集的一些資料,同時感謝提供這些資訊的作者。原文如下 例如,把單一元素的陣列放在乙個struct的尾端,於是每個struct objects可以擁有可變大小的陣列。cod...

深度探索C 物件模型

傳世經典書叢 深度探索c 物件模型 美 stanley b.lippman 斯坦利 b.李普曼 著 侯捷 譯 isbn978 7 121 14952 8 2012年1月出版 定價 69.00元 16開 356頁 宣傳語 如果你是一位c 程式設計師,渴望對於底層知識獲得乙個完整的了解,那麼本書正適合你...

深度探索C 物件模型

傳世經典書叢 深度探索c 物件模型 美 stanley b.lippman 斯坦利 b.李普曼 著 侯捷譯 isbn978 7 121 14952 8 2012年1月出版 定價 69.00元 16開 356頁 宣傳語 如果你是一位c 程式設計師,渴望對於底層知識獲得乙個完整的了解,那麼本書正適合你 ...