類別成員函數指標

類別成員函數指標(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賦值;可以用==運算子、!=運算子。但不允許使用其他的指標算術與比較運算子,如>、<等等。

不能把類的靜態成員函數賦值給類別成員函數指標。類的靜態函數只能賦值給普通函數指標。因為類的靜態成員函數不具有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定義的繼承關係關鍵字把那些不是未知類型的成員函數指標顯式地給出其類繼承關係是單繼承、多繼承、虛繼承,從而使該類的成員函數指標分別是單倍、二倍、三倍的尺寸。

類別成員函數指標的用途

類別成員函數指標的主要用途是把數據與相關代碼結合在一起。這與委託(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 <iostream>
 
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 <<"Test3类成员函数指针长度="<<sizeof(void(Test3::*)()) <<'\n';
std::cout <<"Test4类成员函数指针长度="<<sizeof(void(Test4::*)()) <<'\n';
std::cout <<"Test5类成员函数指针长度="<<sizeof(void(Test5::*)()) <<'\n';	 
std::cout <<"Test类成员函数指针长度="<<sizeof(void(Test::*)()) <<'\n';

//以下可以打开IDE的反汇编(Disassembly)窗口观察成员函数指针的赋值与调用
Test5 a;                                                             //定义一个实例
void (Test5::* pfunc)()=&Test5::foo;                //定义类成员函数指针并赋值
pfunc=&Test5::Test2::foo;
pfunc=&Test2::foo;
pfunc=&Test5::Test3::foo;

(a.*pfunc)();  //调用类成员函数指针,同时使用了虚基表(vbtbl)索引值与this指针调整值
}

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

使用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
  1. ^ "Inheritance Keywords" in MSDN. [2013-06-28]. (原始內容存檔於2016-03-22).