原標題:成員函數指針的一些奇怪行爲

指向成員函數的指針的怪異行爲

提示:以下內容僅僅是針對Microsoft Visual C++編譯器產生的行爲進行介紹,其他的編譯器可能並沒有下文所描述的行爲,所以,不要搞錯了。

如果你只是使用單繼承,則指向成員函數的指針實際上是指向了這個函數的起始地址,因爲在單繼承中,所有基類都共享了同一個this指針。我們看看下面的代碼:

因爲它們都使用了同一個this指針,一個指向基類成員函數的指針可以被當做是指向Derived2的成員函數指針來使用,不需要進行任何的轉換操作。

在單繼承中,指向一個類的成員函數指針的大小就是該平臺指針的大小。

但是如果你使用到了多重繼承,則事情就開始變得有趣起來:

這個時候,就會存在兩個可能的this指針。第一個(p)可以被子類Derived和基類Base1使用,第二個(q)可以被子類Base2使用。

一個指向Base1的成員函數的指針可以被當做一個指向Derived的函數指針來使用,因爲它們都使用了相同的this指針。

但是,一個指向Base2的成員函數指針就不能被當做一個指向Derived的成員函數指針來使用,因爲這個時候,this指針需要做一個微小的移動。

要解決這個問題,有很多不同的方法。Visual Studio編譯器是這樣處理的。

指向一個多重繼承的子類的成員函數指針,實際上是被定義到了一個結構體中,如下圖所示:

一個多重繼承的子類的成員函數指針的大小爲指針的大小加上一個size_t的大小。

如果我們比較多重繼承和單繼承,則你會發現:成員函數指針的大小會隨着具體類對象的不同而不同。

爲了通過成員函數指針來調用成員函數,我們需要調整this指針,調整之後才能正確地調用這個成員函數。下面是具體的一個例子:

在上面的代碼中,adjustor在什麼時候會爲非零值呢?

考慮上面提到的例子。成員函數Derived::Base2Method()實際上也等同於Base2::Base2Method(),所以它會期望接收q作爲它的this指針。爲了將p轉換爲q,adjustor必須知道sizeof(Base1)的值,所以當Base2::Base2Method()的第一行執行的時候,它會收到預期的q作爲它的this指針。

“那,爲什麼不簡單使用一個thunk,這樣就不用手動地調整指針了?” 如下圖所示:

然後使用這個thunk作爲函數指針。

原因在於:存在函數指針的轉換。

讓我們考察如下的代碼:

我們從一個指向Base2成員函數的指針開始,Base2是僅使用單一繼承的類,因此它僅包含一個指向代碼的指針。要將其分配給使用多個繼承的成員函數Derived的指針,我們可以重用函數地址,但是現在我們需要一個adjustor,以便可以將指針“p”正確地轉換爲“q”。

請注意,該代碼不知道pfnBase2指向什麼函數,因此不能僅將其替換爲匹配的thunk。它必須在運行時生成一個thunk,並以某種方式來決定何時可以安全地釋放內存。(因爲這是C++。這裏沒有垃圾收集器。)

還要注意,當pfnBase2強制轉換爲Derived成員函數的指針時,其大小發生了變化,因爲它從僅使用單繼承的類的指針變爲使用多繼承的類的函數的指針。

也即:對一個函數指針進行轉換,可能會改變其大小。

我猜在看到這篇文章之前,你不知道有這回事兒吧?

課後練習題

考察下面的類:

下面的代碼將會如何編譯?

最後

Raymond Chen的《The Old New Thing》是我非常喜歡的博客之一,裏面有很多關於Windows的小知識,對於廣大Windows平臺開發者來說,確實十分有幫助。

本文來自:《Pointers to member functions are very strange animals》

相關文章