類成員函數指針

類成員函數指針（member function pointer），是C++語言的一類指針數據類型，用於存儲一個指定類具有給定的形參列表與返回值類型的成員函數的訪問信息。

**語法**
使用::*聲明一個成員指針類型，或者定義一個成員指針變量。使用.*或者->*調用類成員函數指針所指向的函數，這時必須綁定（binding）於成員指針所屬類的一個實例的地址。例如：
struct X {
void f(int){ };
int a;
};
void (X::* pmf)(int); //一個類成員函數指針變量pmf的定義
pmf = &X::f;            //類成員函數指針變量pmf被賦值
X ins, *p;
p=&ins;
(ins.*pmf)(101);       //對實例ins，調用成員函數指針變量pmf所指的函數
(p->*pmf)(102);      //對p所指的實例，調用成員函數指針變量pmf所指的函數
由於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++標準並沒有明確規定類成員指針在派生類與基類之間的類型轉換。但不允許類成員函數指針與其它無繼承關係的類的成員函數指針互相轉換。不允許與普通函數指針互相轉換。
如果把基類的虛函數賦給派生類的成員函數指針，例如
DerivedClass_Func_to_Mem = & BaseClass::virtualFunc;
實際上是把基類虛表中該虛函數條目對應到了派生類成員函數指針。調用該成員函數指針會執行到哪個函數，需要動態決定。
類成員函數指針可以用0賦值；可以用==運算符、!=運算符。但不允許使用其他的指針算術與比較運算符，如>、__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靜態數組中。
typedef void (CCmdTarget::*AFX_PMSG)(void);
調用用戶類中該消息處理函數時，根據該函數保存在_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函數，轉換過程是：
BOOL (CWnd::*)(WPARAM, LPARAM lParam) => void (CWnd::*)() => void (CCmdTarget::*)()

**例子**
#include 

class Test; //一個未定義的類。
class Test2 
{
int i;
public:
	void foo(){ }
};
class Test3
{
	int i;
public:
void foo(){ }
};

class Test4:public Test2 , public Test3 //多繼承的類 
{
	int i;
public:
void foo(  ) { }
};
class Test5:virtual public Test4 //虛繼承的類 
{
	int i;
public:
void foo(  ) { }
};
int main()
{ 
std::cout 

**未知繼承的成員函數指針例子**
使用Microsoft Visual C++編譯32位程序：
//main.cpp 不需要任何特殊的編譯選項
class Test;                                         //一個forward declaration、未定義的類

typedef void(Test::*NULLFUNCPTR)(); //未知繼承的類成員函數指針的類型定義
extern Test  var;                                 //外部定義的全局對象
extern  NULLFUNCPTR pfunc;           //外部定義的類成員函數指針的變量
void set();                                      //外部定義的對類成員函數指針pfunc初始化
void Helper(Test &var, NULLFUNCPTR pf)
{
	(var.*pf)();
}
int main()
{ 
	size_t ss=sizeof(NULLFUNCPTR);
set();  
	Helper(var, pfunc  );
}
// DefineTest.cpp 
//必須用Visual C++編譯選項/vmg
class t2 
{
int i;
public:
void virtual foo(){ }
};
class t3
{
int i;
public:
t3() { i=101;}
void virtual foo()
{ 
	  printf("In t3::foo()  %d\n",i);
}
virtual void foo3()
{ 
printf("In t3::foo3()  %d\n",i);
}
};
class t4:public t2 , public t3 //多繼承的類 
{
int i;
public:
void virtual foo(  ) { }
};
class Test:virtual public t4 //虛繼承的類 
{
int i;
public:
Test()  {i=102;}
void virtual foo (  ) 
{ 
printf("In Test::foo()  %d\n",i);
}
virtual void bar() { } 
};
/* 類成員函數指針的類型定義。因為使用了/vmg編譯選項，該類Test的成員函數指針類型為16字節長。
如果不使用/vmg編譯選項，在本文件中有類Test的完整定義，所以編譯器會把類Test的成員函數指針類型
按照虛繼承的情形定義為12字節長，
這導致與main.cpp的類Test的未知繼承的成員函數指針類型（16字節長）不匹配，
程序運行時出錯  */
typedef void(Test::*NULLFUNCPTR)(); 
Test var;                             //全局對象，用於main.cpp
NULLFUNCPTR pfunc;       //全局成員函數指針，用於main.cpp
		
void set() //初始化pfunc
{
	size_t ss=sizeof(NULLFUNCPTR); //長度是16；如果不用/vmg編譯選項則為12
/* 賦初值。pfunc的16字節依次存入了
字節0-3：t3::foo3的函數體開始地址；
字節4-7：值8（表示多繼承情形下從Test::t4到Test::t3的偏移量）；
字節8-11：值4（表示從Test實例的首地址到Test的虛基類表指針vbptr的偏移值vtordisp；
如果沒有定義Test::bar，則Test沒有自己的虛函數表，此偏移值為值0. 
即vbptr在類對象的開始地址可能為0或4，
所以對於未知繼承情形，必須保存虛基類表指針vbptr相對於對象首地址的偏移值）；
字節12-15：值4（表示在虛基類表vbtbl中，
保存了從虛基類Test::t4到Test::vbptr的地址偏移量的條目的字節位置）。
Visual C++編譯器對多重繼承且虛繼承的對象的基類部分的地址的計算表達式：
this+ *( *(this+虛基表指針的地址調整值) + 虛基表中條目的字節位置) + 多重繼承的地址調整值
*/
	pfunc=&t3::foo3;   
/* 上述語句右端是&t3::foo或者&Test::t3::foo，pfunc實際對應到了
Test::t3的虛表的第一項所保存的函數，即Test::foo()的thunk（用於在調用
Test::foo()之前，把this指針從指向Test::t3調整到指向Test實例的開始地址） */
}

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