成员变量/函数指针的用法

标准C++并没有真正的面向对象的函数指针。这是很遗憾的,因为面向对象的函数指针(有时也称为委托)已经在其他语言中被证明了它的价值。在Delphi(面向对象的Pascal语言)中,面向对象的函数指针是Borland公司构建VCL(可视化组件库)的基础。最近,C#为了显示其语言本身的成功,也在力推委托(delegate)的概念。对于很多应用程序来说,委托简化了一些使用松耦合对象构建的设计模式的使用(如观察者模式Observer、策略模式Strategy、状态模式State,注:这些模式出自四巨头写的《设计模式:可复用面向对象软件的基础》一书)。毫无疑问,面向对象的函数指针对于标准C++也是很有用的。

C++没有委托的概念,它只提供成员函数指针。大部分C++程序员从来没有使用过成员函数指针,而且他们有很好的理由。因为成员函数指针有很奇异的语法(如->* ,.*),一般程序员很难确切理解他们的意思,况且大部分事情可以通过其他方式来实现。这里有一点误解:事实上,对于编译器来说,实现一个适当的委托比实现成员函数指针要容易得多。

本文,我将为大家揭开成员函数指针的神秘面纱。首先我会介绍成员函数指针的语法和特性,然后我再解释成员函数指针是如何在一般的编译器里被实现的,还有编译器如何高效地实现委托。最后,我将展示我是如何使用这些关于成员函数指针的鲜为人知的知识,来实现在大部分C++编译器上能有很高效率的委托。打个比方说,在Visual C++(6.0或.NET或.NET 2003)调用一个单目标的委托,将仅仅需要产生两行汇编代码!


函数指针

我们先来看一下函数指针。在C中,乃至后来的C++中,一个指向带一个int参数和一个char *参数、返回一个float值的函数指针(暂定为my_func_ptr)可能被声明如下:

 

注意,不同的参数组合的函数指针,类型是不一样的。在微软的VC中,不同的函数调用协议(calling conventions)也会导致函数指针类型的不同。这些函数调用协议包括__cdecl、__stdcall和 __fastcall。你可以如下使用函数指针指向一个函数float some_func(int, char *):

 

当你以后想调用这个函数时,可以这么做:

 

函数指针的类型之间是可以相互转换的。但将函数指针转型为void *却是不允许的。其它的操作这里就不作介绍了。一个函数指针可以赋值为0,来表示这是一个空指针。函数指针还可以进行一系列比较操作(可以使用比较符(==, !=, <, >, <=, >=))。你可以通过==0或隐式转型为bool来判断一个函数指针是否为空指针。有趣的是,函数指针可以被当作无类型模板参数来使用。这跟类型参数有着根本的区别,跟整体的无类型参数也是不同的。它根据名字来被实例化,而不是根据类型或值。基于名字的模板参数不被任何编译器所支持,甚至也不被所有其他的支持部分模板专用的模板所支持。

在C中,函数指针最通常地被用作一些库函数(如qsort)的参数,Windows函数的回调等。当然,函数指针还有很多其它的应用。函数指针的实现很简单:他们就是代码的指针,他们保存了汇编语言函数的起始地址。不同的函数指针类型的存在,仅仅是为了保证函数在被调用时使用正确的调用协议。

 

成员函数指针

在C++程序中,大多数函数都是成员函数。也就是说,他们是一个类的一部分。使用一个普通的函数指针指向一个成员函数是不允许的。正确的做法是,你必须使用一个成员函数指针。一个指向SomeClass类的一个成员函数的成员函数指针可以被声明如下(函数参数跟前面一样):

 

注意到我们在上面的声明中用到了(::*) ,也就是SomeClass成为了声明的一部分。成员函数指针有一个讨厌的限制:他们只能用来指向一个类内的成员函数。不同的参数组合要使用不同类型的成员函数指针。在MSVC中,不同的函数调用协议(包括__cdecl、__stdcall、__fastcall和__thiscall,__thiscall是默认值。有趣的是,__thiscall关键字没有文档说明。有时如果你显式地使用它,会得到一个错误信息,指示这个关键字是为将来使用而保留的)也要使用不同的函数指针类型。如果你使用成员函数指针,建议你使用typedef来避免不必要的混淆。

可以如下指向函数float SomeClass::some_member_func(int, char *) :

my_memfunc_ptr = &SomeClass::some_member_func;

大部分编译器(比如MSVC)允许你省略&,但有一些(比如GNU G++)不允许省略。因此如果你想你写的代码具有良好的移植性,请保留&。调用一个成员函数指针,你需要提供SomeClass的实例,而且你必须使用特殊的操作符->*。这个操作符的优先级很低,因此要用括号括起来:

 

不要因为这种语法而指责我,看起来,c++的设计者很热衷于标点符号。

为了支持成员函数指针,c++比c语言增加了3个特殊的操作符。::*被用作声明指针,->* 和.* 被用作调用指针所指向的函数。看起来,这个语言中晦涩的、很少被使用的特性已经得到了非常的关注。(你甚至可以重载->*操作符,尽管你为什么这么做的原因已经超过了我的想象。我只是知道有这种用法而已。)

一个成员函数指针可以被设为0,可以提供==和!=操作,但仅限在同一个类的成员函数指针之间。任何成员函数指针都可以跟0比较,以此来判断是不是空指针。与一般的函数指针不同,不等比较符(<, >, <=, >=)不能用在成员函数指针之间。跟函数指针一样,成员函数指针可以被用作无类型模板参数(编译器可能要打补丁)。


诡异的成员函数指针

成员函数指针还有一些诡异的地方。首先,你不能用一个成员函数指针指向一个静态(static)的成员函数,而必须使用一个一般的函数指针。(因此,“成员函数指针”这个名字有点误导:事实上他们只是“非静态成员函数指针”。)其次,当处理派生类的时候,会有几处让你吃惊的地方。例如,下面的代码(不要改动注释部分)将在MSVC上编译通过:

 

很奇怪的是,&DerivedClass::some_member_func是SomeClass类的一个成员函数指针,但它不是DerivedClass类的成员。(一些编译器的行为有些细小的差别:比如对于Digital Mars C++来说,这种情况下的&DerivedClass::some_member_func是无定义的。)但是,如果DerivedClass类重新实现虚函数some_member_func,上面的代码将无法编译通过,因为&DerivedClass::some_member_func现在变成了一个DerivedClass类的成员函数指针。

成员函数指针之间的转换是一个异常黑暗的区域。在C++标准化进程中,关于是否能够将一个成员函数指针从一个类转化到他的基类或者派生类的一个成员函数指针,以及是否能在两个不相关的类之间实现转换,存在着很多的争论。然而到标准委员会做出决定的时候,一些编译器开发商早已经有了他们自己的实现,他们对这些问题给出了自己的答案。根据标准5.2.10/9部分,你可以使用reinterpret_cast来帮助在一个类的一个成员函数指针中存储另外一个不相关类的一个成员函数。执行这种转化后的成员函数的结果是无法预料的。你唯一能做的,就只有将成员函数指针仍然转换成它原来的类型(原来是哪个类的还是转化回哪个类)。我会在下文继续讨论这个问题,因为这是个标准与实际的编译器相去甚远的部分。

在一些编译器中,诡异的事情甚至会在基类和派生类的成员函数指针之间转换时发生。当你使用了多重继承,使用reinterpret_cast将成员函数指针从一个派生类转换到一个基类可能不能被编译通过,这取决于你的派生类在声明时安排基类的顺序!这里有个例子:

 

对于case (a),static_cast<Base1mfp>(x)可以工作,但static_cast<Base2mfp>(x) 将会失败。然而case (b)的情况恰好相反。你只可以安全地将成员函数指针从派生类转换到第一个基类!你可以试一下,MSVC会抛出C4407的警告信息,而Digital Mars C++会引发一个错误。另外,两个编译器都不允许你使用reinterpret_cast来代替static_cast(为什么不允许这样做的原因各有不同)。然而,一些编译器无论你怎么做都不会提出异议。小心哪!

标准中还有另外一个有趣的法则:你可以在一个类被定义之前声明它的一个成员函数指针。这给一些编译器会带来一些预料不到的影响(我们后面再讨论)。如果可以的话,请你尽量避免这样做。

值得注意的是,和成员函数指针一样,C++标准也提供了成员数据指针。他们使用相同的操作符,一些实现问题也是一样的。他们在stl::stable_sort的实现中被用到了,但除此之外,我不知道他们还有其他什么有价值的用途。

 

成员函数指针的使用

到目前为止,我大概已经让你确信成员函数指针是一种有点奇异的东西。但他们到底有什么用呢?我在网上进行了大量的搜索,从网上发布的代码中大致发现成员函数指针的两种主要的使用方式:

a. 做作的例子,用于给C++的初学者演示C++的语法
b. 实现委托(Delegate)

当然还有一些微不足道的应用,如STL中的单行函数改编器和boost库(允许你使用成员函数来使用标准的算法)。在这些情况下,他们是在编译时被使用的;通常,函数指针不会出现在编译生成的代码中。成员函数指针最有趣的应用莫过于定义复杂的接口。一些重要的事情可以通过这种方式来实现,但我没有找到很多这样的例子。大多数时候,这些工作可以通过更为优雅的虚函数来完成,或者进行问题的重构。但到目前为止,成员函数指针最有名的应用是在各种应用程序的框架中。他们组成了MFC消息系统的核心。

当你使用MFC的消息映射宏(比如ON_COMMAND)的时候,你实际上提供了一个包含消息ID和成员函数指针的数组(指定为CCmdTarget::*成员函数指针)。这就是为什么MFC类如果想要处理消息的话必须从CCmdTarget类派生的原因。但是各种消息处理函数有不同的参数列表(例如OnDraw函数将CDC *作为它的第一个参数),因此那个数组必须包容各种类型的成员函数指针。MFC是如何来处理的呢?他们使用一个可怕的租借(hack),将所有可能的成员函数指针放到一个
巨大的联合体中,以此来搅乱C++通常的类型检查。(可以到afximpl.h和cmdtarg.cpp中查看MessageMapFunctions联合体,“血淋淋”啊! )因为MFC是如此重要的一块代码,实际上,所有的C++编译器支持这个租借。

在我搜索的过程中,我找不到很多关于成员函数指针的除了编译时的应用外的出色的应用例子。因为他们的复杂性,他们对语言的增值并不大。它最终难逃这样的一个结论:C++的成员函数指针的设计是存在缺陷的。

在写这篇文章的时候,我有一个主要的观点:C++标准允许你在成员函数指针之间进行转换,但转换成功后不允许你调用他们,多么可笑!可笑在下面三个原因。第一,转换在很多流行的编译器上不总是能够工作(也就是说,转换是标准的,但不是可移植的)。第二,在所有的编译器上,如果转换是成功的,调用转换后的成员函数指针的结果将符合你所期望的:(标准中)没有必要将其归类为“无定义行为”。(祈祷?是可移植的,但不是标准的!)第三,允许转化但不允许祈祷(invocation)是一无用处的。如果转换和祈祷都是可能的,那么有效的委托是很容易实现的。这将给语言带来巨大的价值。

 

 

指针是指向一些内存地址的变量,既可以是数据的地址也可以是函数的地址。C++的成员指针遵从同样的原则。困难的是所有的指针需要一个地址,但在类内部没有地址;选择一个类的成员意味着在类中偏移。只有把这个偏移和具体对象的开始地址结合,才能得到实际地址。成员指针的语法要求选择一个对象的同时逆向引用成员指针。

 

如果有一个这个结构的指针sp和对象so,如果有一个指针指向一个类对象成员,甚至
假设它代表对象内一定的偏移,将会发生什么?为了取得指针指向的内容,必须用*号逆向引用。但是,它只是一个对象内的偏移,所以还必须要指定那个对象。因此,*号要和逆向引用的对象结合。

 

定义pm的语法是什么?其实它像任何一个指针,必须说出它指向什么类型。并且,在定义中也要使用一个‘*’号。唯一的区别只是必须说出这个成员指针使用什么类的对象。当然,这是用类名和全局操作符实现的:

定义成员指针:
 

定义并初始化成员指针:

因为引用到一个类而非那个类的对象,因而,&simple::a仅可作为成员指针的语法表示。


指向函数的指针定义像下面的形式:int(*fp)(float); (*fp)的圆括号用来迫使编译器
正确判断定义。没有圆括号,这个表达式就是一个返回int*值的函数。为了定义和使用一个成员函数的指针,圆括号扮演同样重要的角色。假设在一个结构内有一个函数:

 

通过给普通函数插入类名和全局操作符就可以定义一个指向成员函数的指针:
int(simple2::*fp)(float);

初始化:
int(simple2::*fp)(float) = &simple2::f;
&号是可选的;可以用不带参数表的函数标识符来表示地址:fp = simple2::f;

使用:
simple2 s2; 
int i = (s2.*fp)(1.5);

另一个使用例子
 

在构造函数中,成员指针的初始化似乎被过分地指定了。是否可以这样写:
fptr[1] = f2; 因为名字f2在成员函数中出现,是否可以自动地认为在这个类范围内呢?问题是这不符合成员函数的语法,语法要求编译器能够判断将要进行什么。当成员函数被逆向引用时,它仍被过分地指定了,this似乎多余。正如前面所讲的,当它被逆向引用时,语法也需要成员指针总是和一个对象绑定在一起。

 

大胆使用指向成员函数的指针 
指向成员函数的指针是C++中比较复杂的语法结构,然而,它却是事件驱动和多线程环境中不可缺少的,特别是当从外部调用成员函数的时候。在多线程里,每一个线程都通过指向成员函数的指针调用这一函数。如果C++没有这些语法特性,那么在很多情况下进行C++程序开发会遇到很多麻烦。

也许你开始会被这一性质的语法吓倒,但当你熟悉之后你就会觉得它相当方便,并且可以通过使用typedef声明来简化。这一节里我将讲述如何声明一个指向成员函数的指针,赋一个值给它,通过这一指针调用函数。

声明一个指向成员函数的指针

一个指向成员函数的指针包括成员函数的返回类型,带::符号的类名称,函数参数表。虽然这一语法看似复杂,其实它和普通的指针是一样的。指向外部函数的指针可如下声明:

 


相应指向类A的成员函数的指针如下表示:
 

以上pmf是指向类A的一个成员函数的指针,传递两个变量char *和 const char *,没有返回值。注意星号前面的A::符号,这和前面的声明是一致的。

 

赋值

为了给一个指向成员函数的指针赋值,可以采用成员函数名并再其前面加一个&的方式,代码范例可参见Listing A。虽然一些旧的编译器可以忽略&号,但在标准C++中是不允许的。

使用typedef

你可以使用typedef来隐藏一些指向成员函数的复杂指针。例如,下面的代码定义了一个类A中的成员函数的指针PMA,并传递char *const char *参数。

typedef void(A::*PMA)(char *, const char *);
PMA pmf= &A::strcat; // use a typedef to define a pointer to member

使用typedef特别有用,尤其是对于指向成员函数的数组指针。

 

通过指针来调用成员函数

指向成员函数的指针可以调用对象的成员函数而无需知道这一函数的名称。例如一个发送函数,它通过pmf来调用一函数,这一函数与是否指向strcpy() strcat()无关。这与传统的指针调用外部方法是不一样的。传统的指针调用方法是这

样的,你必须指定调用的成员函数所在的具体的对象。

为了能清楚说明这一点,让我们来看一个例子。假设你有两个类A的对象,如Listing B。指向成员函数的指针即有多态性,这样如果你通过这一指针来调用虚成员函数时,这一调用即是动态的。

高级使用方法

掌握了最基本的用法后,让我们看看一些高级的用法:

指向成员函数的数组指针

在下面的例子中,我声明了指向成员函数的两个指针的数组,并给它们赋于类的成员函数的地址:

 

这样的数组在菜单驱动程序非常有用,如表C Listing C所示。

固定的成员函数

指向成员函数的类型包括成员函数的名称,成员函数固定/不固定的性质。只要成员函数是可变的,pmf 会指向任一成员函数。所以,如果你想把touppercase()的地址赋于pmf ,程序会发生错误,因为touppercase()是固定的,Listing D显示了这一例子的代码。

虽然有些编译器能允许非固定指针指向一个固定的成员函数,但在标准C++中是不允许的。

结论

指向成员函数的指针实质上是包含多种数据成员的复杂数据结构。刚开始时你也许觉得它深不可测,但一旦你掌握并习惯了这一语法,你会觉得这一方法在程序设计中是必不可少的,特别是在事件驱动和多线程设计中频繁调用函数的时候。

在MFC中的消息处理函数对应关系中应用了成员函数指针,从基类中调用了派生类的成员函数。大概的方法如下:
 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值