類別成員函式指標

**類別成員函式指標**（member function pointer），是 C++ 語言的一類指標資料類型，用於儲存一個指定類具有給定的形參列表與返回值類型的成員函式的存取資訊。

## 語法

使用 `::*` 聲明一個成員指標類型，或者定義一個成員指標變數。使用 `.*` 或者 `->*` 呼叫類別成員函式指標所指向的函式，這時必須繫結（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 個繼承關係關鍵字用於在類別定義時，顯式規定該類的成員函式指標的長度及儲存在其中的資訊類別。如果在一個原始檔（編譯單元）中在沒有一個類的定義的情況下呼叫了該類的未知類型（unknown）成員函式指標，顯然必須在其他原始檔中對該未知類型（unknown）成員函式指標給出類型定義並賦值，這就必須使用編譯選項 `/vmg` 來編譯此原始檔。`/vmg` 編譯選項使得編譯單元中所有的類別成員函式指標均為四倍尺寸。可以用上述 3 個 Microsoft 定義的繼承關係關鍵字把那些不是未知類型的成員函式指標顯式地給出其類繼承關係是單繼承、多繼承、虛繼承，從而使該類的成員函式指標分別是單倍、二倍、三倍的尺寸。

## 類別成員函式指標的用途

類別成員函式指標的主要用途是把資料與相關代碼結合在一起。這與委託（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 函式，轉換過程是：

## 例子

### 未知繼承的成員函式指標例子

使用 Microsoft Visual C++ 編譯 32 位元程式：

 

## 參考文獻

* 《ISO/IEC 14882:2011 C++ Standard》 8.3.3 Pointers to members