C++對象模型筆記:dynamic binding

 

C++對象模型筆記:dynamic binding

 

編譯器對於多態的實現是怎樣的呢?下面請看一個例子:

 

  

編譯器會怎麼做呢?用上一篇筆記裏面的name mangling是不行的。

 

當然,在這個例子裏面,如果你編譯的時候用優化選項,編譯器也許會把上面三條語句優化如下:Point2D pt2d; pt2d.print( ); !!!你也許會驚訝:編譯器這麼牛?!是的,編譯器會分基本快,然後對每一個基本塊進行優化合並;(相關知識,請參考編譯原理,我也已經忘的差不多了);

 

但是但對於下面的例子,估計再牛的編譯器也沒有辦法:

 

 

 

可以看到,如果不用點措施,犧牲一點東西,printPoint裏面是不知道那個Point指針的所指向的真正對象是哪個的。那麼,怎麼辦呢?(換了是你,你說怎麼辦?)

 

如果某種技術解決不了某些問題,原因就是在這些問題裏面還有一些信息是某些技術所沒有用到的。這就是技術的一般方法論(出處在我這裏,呵呵)

 

那麼,根據上面的方法論的指導,只需要再增加某些信息,然後再增加某些中間層,把信息放到中間層裏面去,也許就可以解決(廢話

 

這雖然是廢話,但是也顯示了多態的實質。所以就叫做顯示多態實質的廢話吧。

 

具體如下:

 

1、編譯器遇到了class Point的定義的時候,發現有裏面有virtual的成員函數,於是將這個類的定義轉換如下:

 

 

2、對Point2D的轉換如下的僞代碼所示:(注意,雖然Point2D的定義裏面沒有定義vritual,但是其基類Point有成員函數定義了virtual,所以還是有虛函數表,即使Point2D什麼都沒有寫,如下所示:class Point2D : public Point{}也有虛函數表,Point2D的每一個對象也還會有vptr成員。

 

 

 

  

那麼下面的語句:

就會變成類似於下面的僞代碼:(C式的,不是C++式的)

 

 

 

看了之後是不是覺得有點無語啊,怎麼虛函數的調用原來這麼麻煩!!看來還是C語言好啊,起碼不會做這麼多事……

 

注意,不是這樣的,以上的過程都是在編譯階段就做好了的,那些虛函數表在編譯階段就已經做好了。所以對於多態的執行的代價如下:

 

1、  對於空間來說,每一個定義了virtual的類,都在全局數據區裏面有一張虛函數表,虛函數表的大小決定於這個類的體系(就是這個類及其基類)中虛函數的個數。(這張表是在編譯階段就已經定義好了)

 

2、  以上類的每一個對象實例,在空間上多了一個指針的空間。

 

3、在類的構造函數裏面,多了一條語句的開銷(這條語句就是初始化上面多出來的指針,指向相應類型的虛函數表),這個要留意,如果類中沒有聲明構造函數,這個時候,編譯器會自動生成一個(說到這裏,不要以爲編譯器無論在什麼時候都會爲你的類生成一個默認構造函數啊~,以後的筆記會對這個問題重點討論),因此,還多了一個你可能並不想要的調用函數的開銷(當然,也可能是以內聯的方式嵌到代碼當中,這個就要看編譯器的能力了)

 

4、  在執行語句p->print();的時候,由於編譯器已經轉換爲(p->vptr_point)[0] )(pt)實際上多個間接層,看出來沒有,一般的p->f()只需要f_Point()…做全局調用就可以了(name mangling轉換),現在卻要對p尋址,尋址了還要找vptr_point在取它指向的0-4的字節,然後再調用那個地址….說起來好像間接層不止一個……

 

以上的4點就是c++中虛函數調用的運行時候所付出的幾乎所有代價。

 

所以以後參加面試的時候,有人問起:class A , A *p; … p->func() 的內部代價是怎樣的?你一定一定要答詳細一點,有多詳細就答多詳細,最好能說出前因後果,不要像我一樣,當時就答:“當funcA的虛函數的時候,代價會大一些……是不是無語了,呵呵

 

(注:他當時問的是:a.func() pa->func()在實現上有什麼不同。其實和上面的問題是一樣的,我心裏也知道有什麼不同,只不過答的時候就說了一句話……

 

這種運行時才查表來調用函數的機制,被稱爲動態綁定,好像很有術語的味道,但其實也就這麼回事而已,天下事有難易乎

 

關於這種虛函數表的機制的內存佈局圖,這裏就不畫了,在我上面的筆記:三種內存佈局裏面有圖;

 

還有兩個地方需要說一下的:

 

1、  虛函數的實現機制不止一種,其實還有幾種機制;不過這一種最高效(c++的目標之一啊),所以幾乎所有的編譯器都用了這種方法,當然,這種方法也是有缺點的,請參考我的“MFC消息映射原理”,或者是別的文章。

 

2、關於虛函數表的表項和函數的入口地址關係,在這裏似乎用了一種硬編碼的方法,比如索引0的表項放的是print函數的,1放的是**函數的而且所有派生類的虛函數表的表項也得這麼做,這個順序應該是按照類的定義裏面那些虛函數的聲明次序來的。而且,如果派生類有新的虛函數,這些新的虛函數要在虛函數表中往後插(不能前插,因爲前面已經是硬編碼了,注意);這個我沒有看過別人的見解,是我自己推測出來的,不過想來也應該如此,如有不對之處,請各位多多指教,小弟不勝感激。

 

3、關於各家編譯器實現的差異,關於vptr在對象中的安插位置,不同的編譯器中可能不同,比如g++ 3.4.3將其插入到每個對象的最前面一項,在vs2005中是插在最後面一項的。至於有沒有插到中間的,我就不知道了。其實有一種情況是插到中間的,這個到討論多重繼承的多態性時候再說。

 

 

下面的代碼有個疑問,大家不妨看看:

 

  

編譯器遇到這種情況,又是怎麼做的呢?以上的情況,到底屬於怎樣的一種情況呢,請看下篇筆記,隱藏和二義性~,謝謝各位觀光,呵呵。

 

PS. 寫了這麼多篇筆記,關於《深度探索C++對象模型》的內容還沒有正式開始討論呢,還是在熱身階段,本來想看看大家對我寫的這些東西有什麼反饋的,哪知道一點反饋都沒有,要麼就是灌水的貼,比如“寫的好啊”之類的回帖。

 

那就有兩種可能,一是高手看了覺得我的文章錯漏百出,不屑一看;二就是大家看不懂我的文章到底在寫些什麼東西,覺得莫名其妙;

 

但是,這些文章代表的是我現在對c++的理解,如果各位發現了問題能告訴我一下,我實在是不勝感激,哪怕是評論說:“你這些文章文筆太爛,語句不通,一點都看不懂”都會對我有所幫助。

 

因爲我覺得一個互動的平臺,比一個單獨的閱讀和筆記更有利於提高雙方的能力和理解。

 

另外一個,是我覺得,學習別人文章的態度,並不是一味的全盤吸收,而應該是有所懷疑,在懷疑的基礎上在加以論證,從懷疑出發,經過驗證,到最後得到結論,這樣的學習的印象會比單獨吸收結論要深的多,而且不容易偏信;(再次說明,我在這裏的很多推論性的東西都是我自己推猜出來的,沒有經過任何權威的肯定,也沒有看過編譯器源碼去驗證)

 

對人則不然,對人際交流來說應該持相反的態度,是先相信你說的話,經過考察,再慢慢的下結論;

 

但是我們中國人,卻恰好相反,對人是一開始持懷疑的態度,慢慢慢慢的熟悉了,才相信你;對別人網上貼的文章,則是一開始就全盤接收,在經過錯誤的教訓之後,纔開始提出反對意見(這個時候往往有對其作者的辱罵一起的傾向)。

 

以上僅代表我個人的觀點,不敢強迫於大家,再次謝謝各位在百忙之中能閱讀這些筆記。

 

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