定義
類成員函式指針是一類指針數據類型,C++的語法之一,主要用途是把數據與相關代碼結合在一起。這與委託(delegate)、函子(functor)、閉包(closure)等概念很像。雖然C++對此支持的並不太好。MFC類體系中,Windows訊息傳遞處理機制是基於CCmdTarget類及其派生類的靜態數據成員與靜態成員函式GetThisMessageMap()。用戶所寫的類中的Windows訊息處理函式(例如OnCommand)必須轉換為CCmdTarget::*的成員函式指針類型AFX_PMSG,保存在該用戶類的_messageEntries靜態數組中。
調用用戶類中該訊息處理函式時,根據該函式保存在_messageEntries中的signature(一個無符號整型表示的函式的形參類型列表與返回值類型),把類型為void (CCmdTarget::*AFX_PMSG)(void)的成員函式指針強制轉為其它類型的CCmdTarget成員函式指針(例如void (AFX_MSG_CALL CWnd::*pfn_v_i_i)(int, int),在union MessageMapFunctions中列出了近百種CCmdTarget成員函式指針),然後調用轉換後的成員函式指針。這是基於Visual C++編譯器把單繼承的成員函式指針編譯為只保存了函式的記憶體起始地址,因此可以在同一個單繼承類中把一種類型的成員函式指針強制轉換為另一種成員函式指針,或者把單繼承派生類的成員函式指針強制轉換為基類成員函式指針。這是打破了C++標準的違例辦法。例如,對於CWnd::OnCommand函式,轉換過程是:
函式指針不能直接調用類的成員函式,需採取間接的方法,原因是成員函式指針與一般函式指針有根本的不同,成員函式指針除包含地址信息外,同時攜帶其所屬對象信息 。
語法
使用::*聲明一個成員指針類型,或者定義一個成員指針變數。使用.*或者->*調用類成員函式指針所指向的函式,這時必須綁定(binding)於成員指針所屬類的一個實例的地址。例如:
由於C++運算符優先權列表中,函式調用運算符()的優先權高於.*與->*,因此成員函式指針所指的函式被調用時,必須把實例對象或實例指針、.*或->*運算符、成員函式指針用括弧括起來,如上例所示。
C++標準規定,非靜態成員函式不是左值,因此非靜態成員函式不存在表達式中從函式左值到指針右值的隱式轉換,非靜態成員函式指針必須通過&運算符顯式獲得。所以上例中,pmf = X::f; 將編譯報錯。
語義
不同於普通函式,類成員函式的調用有一個特殊的不寫在形參表里的隱式參數:類實例的地址。因此,C++的類成員函式調用使用thiscall調用協定。類成員函式是限定(qualification)於所屬類之中的。
同樣,類成員函式指針與普通函式指針不是一碼事。前者要用.*與->*運算符來使用,而後者可以用*運算符(稱為“解引用”dereference,或稱“間址”indirection)。普通函式指針實際上保存的是函式體的開始地址,因此也稱“代碼指針”,以區別於C/C++最常用的數據指針。而類成員函式指針就不僅僅是類成員函式的記憶體起始地址,還需要能解決因為C++的多重繼承、虛繼承而帶來的類實例地址的調整問題。因此,普通函式指針的尺寸就是普通指針的尺寸,例如32位程式是4位元組,64位程式是8位元組。而類成員函式指針的尺寸最多有4種可能:
單倍指針尺寸:對於非派生類、單繼承類,類成員函式指針保存的就是成員函式的記憶體起始地址。
雙倍指針尺寸:對於多重繼承類,類成員函式指針保存的是成員函式的記憶體起始地址與this指針調整值。因為對於多繼承類的類成員函式指針,可能對應於該類自身的成員函式,或者最左基類的成員函式,這兩種情形都不需要調整this指針。如果類成員函式指針保存的其他的非最左基類的成員函式的地址,根據C++標準,非最左基類實例的開始地址與派生類實例的開始地址肯定不同,所以需要調整this指針,使其指向非最左基類實例。
三倍指針尺寸:對於多重繼承且虛繼承的類。類成員函式指針保存的就是成員函式的記憶體起始地址、this指針調整值、虛基類調整值在虛基表(vbtable)中的位置總計3項。以常見的“菱形虛繼承”為例。最派生類多重繼承了兩個類,稱為左父類、右父類;兩個父類共享繼承了一個虛基類。最派生類的成員函式指針可能保存了這四個類的成員函式的記憶體地址。如果成員函式指針保存了最派生類或左父類的成員函式地址,則最為簡單,不需要調整this指針值。如果如果成員函式指針保存了右父類的成員函式地址,則this指針值要加上一個偏移值,指向右父類實例的地址。如果成員函式指針保存了虛基類的成員函式地址,由於C++類繼承的複雜多態性質,必須到最派生類虛基表的相應條目查出虛基類地址的偏移值,依此來調整this指針指向虛基類。
四倍指針尺寸:C++標準允許一個僅僅是聲明但沒有定義的類(forward declaration)的成員函式指針,可以被定義、被調用。這種情況下,實際上對該類一無所知。這稱作未知類型(unknown)的成員函式指針。該類的成員函式指針需要留出4項數據位置,分別用於保存成員函式的記憶體起始地址、this指針調整值、虛基表到類的開始地址的偏移值(vtordisp)、虛基類調整值在虛基表(vbtable)中的位置,總計4項。
C++標準並沒有明確規定類成員指針在派生類與基類之間的類型轉換。但不允許類成員函式指針與其它無繼承關係的類的成員函式指針互相轉換。不允許與普通函式指針互相轉換。
如果把基類的虛函式賦給派生類的成員函式指針,例如
實際上是把基類虛表中該虛函式條目對應到了派生類成員函式指針。調用該成員函式指針會執行到哪個函式,需要動態決定。
類成員函式指針可以用0賦值;可以用==運算符、!=運算符。但不允許使用其他的指針算術與比較運算符,如>、<等等。
不能把類的靜態成員函式賦值給類成員函式指針。類的靜態函式只能賦值給普通函式指針。因為類的靜態成員函式不具有this指針,不採用thiscall調用協定,實際上是限定於類作用域的普通函式。 所以,確切地說,應該稱“類非靜態成員函式指針”。
對於g++編譯器,不支持把虛基類的成員函式指針賦給派生類的成員函式指針。也即,g++不支持在虛繼承關係下的成員函式指針的upcast。這大大簡化了g++成員函式指針的實現難度。g++編譯出來的成員函式指針長度都是8位元組,其中的高4位元組是用於多重繼承時調整this指針的偏移值,單繼承時該值為0;低4位元組是個union結構,對於非虛成員函式就是函式體的記憶體起始地址,對於虛函式是該函式在虛表(vtable)中的地址位元組偏移量再加上1。這是因為,函式體的記憶體起始地址起碼是4位元組邊界對齊,所以該值是4的的倍數;而虛表中每個條目是4位元組長度(對於32位程式),虛函式所對應的虛表條目在虛表中的按位元組計算的偏移量也是4的倍數,加上1後就是個奇數。從而可以區分非虛函式與虛函式兩種情形。
Microsoft Visual C++編譯器支持在虛繼承關係下的成員函式指針的upcast。這大大複雜化了該編譯器的成員函式指針的實現。Visual C++定義了三個關鍵字:__single、__multi、__virtual_inheritance分別對應於類是單繼承、多重繼承、虛繼承關係;此外還有第四種情況:類在提前聲明(forward declaration)時的未知類型(unknown)成員函式指針。上述四種情況,Visual C++編譯出的32位程式的成員函式指針長度分別是4位元組、8位元組、12位元組、16位元組。上述3個繼承關係關鍵字用於在類定義時,顯式規定該類的成員函式指針的長度及保存在其中的信息類別。[1]如果在一個源檔案(編譯單元)中在沒有一個類的定義的情況下調用了該類的未知類型(unknown)成員函式指針,顯然必須在其他源檔案中對該未知類型(unknown)成員函式指針給出類型定義並賦值,這就必須使用編譯選項/vmg來編譯此源檔案。/vmg編譯選項使得編譯單元中所有的類成員函式指針均為四倍尺寸。可以用上述3個Microsoft定義的繼承關係關鍵字把那些不是未知類型的成員函式指針顯式地給出其類繼承關係是單繼承、多繼承、虛繼承,從而使該類的成員函式指針分別是單倍、二倍、三倍的尺寸。