類成員函式指標

類成員函式指標（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++ 標準並沒有明確規定類成員指標在派生類與基類之間的型別轉換。但不允許類成員函式指標與其它無繼承關係的類的成員函式指標互相轉換。不允許與普通函式指標互相轉換。

如果把基類的虛擬函式賦給派生類的成員函式指標，例如

DerivedClassFuncto_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::* 的成員函式指標型別 AFXPMSG，儲存在該使用者類的messageEntries 靜態陣列中。

typedef void (CCmdTarget::*AFX_PMSG)(void);

呼叫使用者類中該訊息處理函式時，根據該函式儲存在messageEntries 中的 signature（一個無符號整型表示的函式的形參型別列表與返回值型別），把型別為 void (CCmdTarget::AFXPMSG)(void) 的成員函式指標強制轉為其它型別的 CCmdTarget 成員函式指標（例如 void (AFXMSGCALL CWnd::pfnvi_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

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

使用 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 例項的開始地址） */}