类成员函数指针

类成员函数指针(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).