類成員函數指針
類成員函數指針(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
- ^ "Inheritance Keywords" in MSDN. [2013-06-28]. (原始內容存檔於2016-03-22).