成員函數指針的一些奇怪行為

2020-12-19 漫漫開發路

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

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

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

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

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

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

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

一個指向Base1的成員函數的指針可以被當作一個指向Derived的函數指針來使用,因為它們都使用了相同的this指針。但是,一個指向Base2的成員函數指針就不能被當作一個指向Derived的成員函數指針來使用,因為這個時候,this指針需要做一個微小的移動。

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

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

一個多重繼承的子類的成員函數指針的大小為指針的大小加上一個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》

相關焦點

  • 知識分享:C 語言函數指針之回調函數
    回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。 回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對該事件或條件進行響應。
  • c++ 內存,虛函數,運算函數,三角函數
    棧內存:在函數中定義的一些基本類型的變量和對象的引用變量都在函數的棧內存中分配,存取速度比堆要快,僅次於直接位於CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。堆內存:用來存放由new創建的對象和數組。在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。
  • C++的智能指針你了解嗎?
    但是經常會遇到我們忘記寫free,導致內存溢出,C++也有類似的情況,為了解決掉我們忘記釋放內存的習慣,C++引入了幾種智能指針,為的就是讓函數可以在正常終止或者異常終止的情況下,改指針的指向的內存都可以處於正確的狀態。shared_ptr、unique_ptr、weak_ptr、auto_ptr。
  • C語言|文件指針、fopen()、fscanf()、fclose()
    這是一個簡單的文件系統fscanf()函數的功能是把磁碟文件數據讀出保存到變量(內存)每一個文件都有自己的FILE結構和文件緩衝區exit(0)是系統標準函數,作用是關閉所有打開的文件參數0表示程序正常結束,非0參數通常表示不正常的程序結束C語言允許同時打開多個文件,不同文件採用不同文件指針指示,但不允許同一個文件在關閉前被再次打開如圖所示,fgets()函數用來從文本文件中讀取字符串,調用格式為:fgets(s,n,fp);其中s可以是字符數組名或字符指針(指向字符串的指針
  • 建哥指針數學如何將提分從「抽象」轉為「具象」
    高中數學知識中,有一種函數叫做抽象函數,是指沒有給出具體解析式,在考試中常利用其性質、概念進行考察的函數。看似沒有缺乏核心內容,但又將函數的定義域,值域,單調性,奇偶性,周期性和圖象集於一身,所以既是高中的常考題型,又對很多同學造成了困擾,失分非常嚴重。
  • C語言函數調用過程中的內存變化解析
    局部變量的作用域為什麼僅限於函數內?這個調用不是指C 語言上的函數調用的語法,而是在內存的視角下,函數的調用過程。本文將從C 語言調用實例,內存視角,反彙編代碼來探討C 語言函數的調用過程,也可以說是C 語言函數調用過程圖解。通過這個C 語言函數調用過程圖解,同學們將會知道,C 語言函數在調用時,內存空間是怎樣變化的。 要想理解這一個過程還好涉及到函數棧幀的概念。
  • 空指針的傳說
    空指針,號稱天下最強刺客。他原本不叫這個名字,空指針原本複姓異常,空指針只不過是他的武器,但他殺戮過多,漸漸地人們只記住了空指針這三個字。天下武功,唯快不破,空指針的針,以快和詭異著稱,稍有不慎,便是傷亡。
  • CSharp構造函數和析構函數的理解
    (1)實例構造函數使用new表達式創建某個類的對象時,會使用實例構造函數創建和初始化所有實例成員變量。當然也可以去創建帶有參數的構造函數,如果一個類中有無參數構造函數也包含帶參數的構造函數,調用的時候根據實例化調用的時候傳遞參數的情況執行相應的方法。(2)私有構造函數私有構造函數是一種特殊的實例構造函數。它通常用在只包含靜態成員的類中。
  • 區塊鏈_Solidity智能合約_函數、事件與日誌03
    >語法簡單、類似javascript不成熟,但版本更新較快,且兼容性不太好具備面向對象特性:封裝、繼承、多態數據類型分類值類型 (值傳遞)、引用類型 (指針傳遞布爾值整型地址(Address)定長字節數組有理數和整型枚舉類型函數
  • STM32Cube HAL庫中斷處理機制,回調函數實現原理
    所以,我們還需要掌握:應用層代碼如何調用HAL庫函數(API接口),以及HAL庫中斷處理機制等相關知識。HAL庫牽涉的內容較多,下面簡單描述一下HAL庫中斷處理,以及相關的回調函數。1HAL庫中斷處理機制之前使用標準外設庫開發時,中斷程序(函數)由我們自己實現。
  • 標準庫函數與基於HAL庫函數
    在這些 .c .h文件中,包括一些常用量的宏定義,把一些外設也通過結構體變量封裝起來,如GPIO口時鐘等。所以我們只需要配置結構體變量成員就可以修改外設的配置寄存器,從而選擇不同的功能。也是目前最多人使用的方式,也是學習STM32接觸最多的一種開發方式。
  • vxworks中常用的字符串、buffer處理相關函數
    在此列一下vxworks一些常用的字符串、buffer處理相關函數,具體的函數請看幫助:本文引用地址:http://www.eepw.com.cn/article/201610/305791.htma) fioLib.hfioFormatV - 轉換格式字符串fioRead - 會重複調用read()函數直到指定最大長度被讀取或者文件結尾
  • Linux的分離聚合IO函數(scatter/gather)
    然後,Linux就添加了scatter/gather IO函數。常用的有這麼幾個,writev()、sendmsg()、sendmmsg(),readv()、recvmsg()、recvmmsg()。它們使用一個iovec的結構來記錄所需數據的指針和長度,把iovec的結構組成一個數組,就可以一次把多組分離的數據,聚合起來讀寫。既不需要複製數據,也不需要多次調用read/write函數。sendmsg()函數,使用一個msghdr的結構,記錄發送的地址和數據信息。
  • Excel中的一些實用函數技巧,財務計算必備!
    數據文件的處理一定會用到Excel中的函數,這個方面的內容都是比較複雜的。不同的函數對應著不同的處理財務結果。你會使用Excel中的那些函數?今天小編來給大家分享幾個實用的函數,一起來學習學習吧!在彈出的【插入函數】的對話框中,我們在【查找函數】裡輸入YIELD函數,這就是代表返回定期付息有價證券的收益率,找到這個函數後點擊確定。在【函數參數】的對話框中,我們將結算日、到期日、利率、票面價格、清償價格依次輸入到框中。全部輸入好後點擊確定就行了。