版权归原作者所有,如有侵权,请联系我们

[科普中国]-类成员函数指针

科学百科
原创
科学百科为用户提供权威科普内容,打造知识科普阵地
收藏

函数指针是指向函数的指针变量。。函数指针可以像一般函数一样,用于调用函数、传递参数。函数指针只能指向具有特定特征的函数。因而所有被同一指针运用的函数必须具有相同的参数和返回类型。类成员函数指针(member function pointer),是C++语言的一类指针数据类型,用于存储一个指定类具有给定的形参列表与返回值类型的成员函数的访问信息。

定义类成员函数指针是一类指针数据类型,C++的语法之一,主要用途是把数据与相关代码结合在一起。这与委托(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::*)()函数指针不能直接调用类的成员函数,需采取间接的方法,原因是成员函数指针与一般函数指针有根本的不同,成员函数指针除包含地址信息外,同时携带其所属对象信息1。

语法使用::*声明一个成员指针类型,或者定义一个成员指针变量。使用.*或者->*调用类成员函数指针所指向的函数,这时必须绑定(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赋值;可以用==运算符、!=运算符。但不允许使用其他的指针算术与比较运算符,如>、