編譯器又是怎麼實現的呢?


  1. 這樣寫肯定是不推薦的,根據《effective C++》上的解釋,構造和析構階段調動虛函數無法產生多態特性,這往往違背了開發者的本意。
  2. 是不是靜態綁定?我認為也不是。在執行構造函數user code的時候,其實基類和派生類的vftable都已經初始化完成,但此時,基類part的vfptr裝載的是基類的vftable地址,若此時調用虛函數,仍然會去基類虛表裡找jmp地址,因此,找到的是base virtual function。如果你反彙編一下代碼,會發現,在基類part構造完成之後,派生part構造執行compiler code的最後一步,是這樣一條彙編指令:

mov dword ptr [eax],offset derived::`vftable (0FD9B50h)

這一步,是把對象中的vfptr覆蓋成derived::vfptr。因此,在這之後,你才能正確的觸發多態。這種情況和靜態綁定的表現一樣,但我認為不能劃等號,無論如何,它也仍然是通過vftable找函數jmp地址,而不是直接jmp。

上面有朋友說這麼寫代碼是UB,其實這說法是不對的,C++里這麼寫代碼,產生的行為是確定的,一定會調用基類方法,不會產生多態,但也不會有什麼access violation或者crash的問題。相比較而言,java在基類構造函數的compiler code倒是會把虛表和虛指針更新完整,多態倒是沒問題了,但如果這麼玩,就等於是調用未初始化派生對象的方法,產生的行為就是UB了。

總之,無論什麼語言,都不推薦這麼做。


靜態多態請看 CRPT。


構造/析構函數中對this的虛函數調用基本上可以理解為靜態綁定。而對並非指向當前對象的指針/引用仍然使用動態綁定。

gcc對此的實現是:對於構造函數,先調用基類構造函數,然後將this的虛表指針指向本類虛表,再執行初始化列表的成員部分以及構造函數體;對於析構函數,將虛表指針指向本類虛表,然後執行析構函數體。

雖然我只看了gcc的實現,但並不像樓上所說是未定義行為。標準[1]指定:

When a virtual function is called directly or indirectly from a constructor or from a destructor (including during the construction or destruction of the class』s non-static data members, e.g. in a member initializer list), and the object to which the call applies is the object under construction or destruction, the function called is the final overrider in the constructor』s or destructor』s class and not one overriding it in a more-derived class.

有一個小特例:在有多個分支的繼承結構中,對不屬於構造/析構函數所在分支的指針/引用進行虛函數調用,是未定義行為。例如:類A、B虛繼承V,類D繼承A和B,如果在B的構造函數中調用((A*)this)的虛函數,結果就是取決於編譯器的了(gcc編譯結果會調用到B中的實現)。從這裡可以看出,構造/析構函數內仍然是使用動態綁定機制,只不過對虛表指針的處理使得效果與靜態綁定幾乎一致。

參考

  1. ^https://en.cppreference.com/w/cpp/language/virtual#During_construction_and_destruction


當然是動態綁定的。但是此時虛函數表指針指向的是當前正在構造/析構的虛函數表,從而與靜態綁定的行為一致。

至於為什麼是這麼做呢?構造/析構函數內如果調用非虛成員函數,而這個非虛成員函數實現內會使用動態綁定的方式調用虛函數,編譯器無法修改其他非虛成員函數實現的前提下,只能統一實現為動態綁定,表現為靜態綁定。


構造時虛表還沒做出來呢,虛指針可能指向基類的虛表。


從結果上來說,所有正確實現的 C++ 編譯器都會在構造、析構函數中調用虛函數的本地版本,但不能理解為是靜態綁定的。

關於編譯器的大致實現可以看一下我的另一個回答:

C++ 關於子類重寫父類方法,並在構造函數中調用的問題??

www.zhihu.com圖標

或者乾脆讀一下 C++ FAQ 的有關部分,有詳細的討論:

Standard C++?

isocpp.org


這個是未定義行為。我說一下vs下是怎麼做的。

vs下的類的首地址的內容為虛表指針,佔4個或者8個位元組,取決於是32位還是64位程序。

在基類初始化時,虛表指針指向基類虛表,子類初始化時,將其改寫為子類虛表地址。因此在基類中調用虛方法一定是調用了基類的虛函數。

析構函數中也一樣,只不過虛表指針的改寫順序是相反的。

所以無論如何不要在析構函數或者構造函數中調用虛函數。


推薦閱讀:
相关文章