摘自 http://hipercomer.blog.51cto.com/4415661/792300
不错的文章
摘要:这篇文章详细介绍C/C++的函数指针,请先看以下几个主题:使用函数指针定义新的类型、使用函数指针作为参数、使用函数指针作为返回值、使用函数指针作为回调函数、使用函数指针数组,使用类的静态函数成员的函数指针、使用类的普通函数成员的指针、定义函数指针数组类型、使用函数指针实现后绑定以及在结构体中定义函数指针。如果您对以上这几个主题都很了解,那么恭喜您,这篇文章不适合您啦~。在一些开源软件中,如Boost, Qt, lam-mpi中我们经常看到函数指针,本文目的是彻底搞定函数指针的语法和语义,至于怎样将函数指针应用到系统架构中不在此文的讨论范围中。各位看官,有砖拍砖啊~
1. 无处不见的函数指针
使用函数指针可以设计出更优雅的程序,比如设计一个集群的通信框架的底层通信系统:首先将要每个消息的对应处理函数的指针保存映射表中(使用STL的map,键是消息的标志,值是对应的函数指针),然后启动一个线程在结点上的某个端口侦听,收到消息后,根据消息的编号,从映射表中找到对应的函数入口,将消息体数据作为参数传给相应的函数。我曾看过lam-mpi在启动集群中每个结点的进程时的实现,该模块的最上层就是一个结构体,这个结构体中仅是由函数指针构成,每个函数指针都指向一个子模块,这样做的好处就是在运行时期间可以自由的切换子模块。比如某个子模块不适合某个体系结构,只需要改动函数指针,指向另外一个模块就可。
在平时的程序设计中,经常遇到函数指针。如EnumWindows这个函数的参数,C语言库函数qsort的参数,定义新的线程时,这些地方函数指针都是作为回调函数来应用的。
还有就是unix的库函数signal(sys/signal.h)(这个函数我们将多次用到)的声明形式为:
void (*signal)(int signo,void (*func)(int)))(int);
这个形式是相当复杂的,因为它不仅使用函数指针作为参数,而且返回类型还是函数指针(虽然这个函数在POSIX中不被推荐使用了)。
还有些底层实现实际上也用到了函数指针,可能你已经猜到了。嗯,就是C++中的多态。这是一个典型的迟绑定(late-binding)的例子,因为在编译时是无法确定到底绑定到哪个函数上执行,只有在运行时的时候才能确定。这个可以通过下面这个例子来帮助理解:
Shape *pSh;
scanf(“%d”,&choice);
if(choice)
{
pSh= new Rectangle();
}
else
{
pSh= new Square();
}
pSh->display();
对于上面这段代码,做以下几个假设:
(1) Square继承自Rectange
(2) Rectangle继承自Shape
(3) display为虚函数,在每个Shape的子类链中都必须实现
正是因为在编译期间无法确定choice的值,所以在编译到最后一行的时候无法确定应该绑定到那个一个函数上,只能在运行期间根据choice的值,来确定要绑定的函数的地址。
总之,使用指针可以让我们写出更加优雅,高效,灵活的程序。另外,和普通指针相比,函数指针还有一个好处就是你不用担心内存释放问题。
但是,函数指针确实很难学的,我认为难学的东西主要有两个原因:(1)语法过于复杂。(2)语义过于复杂。从哲学上讲,可以对应为(1)形式过于复杂。(2)内容过于复杂。
比如,如果我们要描述“美女”这种动物(老婆不要生气啊~),如果在原始时代,我们可能需要通过以下这种方式:
_____ &&&&_) )
\/,---< &&&&&&\ \
( )c~c~~@~@ )- - &&\ \
C >/ \< |&/
\_O/ - 哇塞 _`*-'_/ /
,- >o<-. / ____ _/
/ \/ \ / /\ _)_)
/ /| | |\ \ / / ) |
\ \| | |/ / \ \ / |
\_\ | |_/ \ \_ |
/_/`___|_\ /_/\____|
| | | \ \|
| | | `. )
| | | / /
|__|_|_ /_/|
(____)_) |\_\_
而现在我们只需要用语言来抽象就行,即用两个汉字“美女”或者英文“beauty”就行了。这就是形式上的简化,也就方便了我们的交流。另外一种就是内容上的复杂度过高,一个高度抽象的表达式后面蕴含着巨大的复杂度对于我们理解问题也是很难的,例如:
P=NP?
由于接触过的书上所讲的关于函数指针方面的都是蜻蜓点水一样,让我很不满足。我认为C/C++语言函数指针难学的主要原因是由于其形式上的定义过于复杂,但是在内容上我们一定要搞清楚函数的本质。函数的本质就是表达式的抽象,它在内存中对应的数据结构为堆栈帧,它表示一段连续指令序列,这段连续指令序列在内存中有一个确定的起始地址,它执行时一般需要传入参数,执行结束后会返回一个参数。和函数相关的,应该大致就是这些内容吧。
2 函数指针简单介绍
2.1 什么是函数指针
函数指针是一个指向函数的指针(呃,貌似是废话),函数指针表示一个函数的入口地址。使用函数指针的好处就是在处理“在运行时根据数据的具体状态来选择相应的处理方式”这种需求时更加灵活。
2.2 一个简单的例子
下面是一个简单的使用函数指针取代switch-case语句的例子,为了能够比较出二者效率差异,所以在循环中进行了大量的计算。
- /*
- *Author:Choas Lee
- *Date:2012-02-28
- */
- #include<stdio.h>
- #define UNIXEVN
- #if defined(UNIXENV)
- #include<sys/time.h>
- #endif
- #define N 1000000
- #define COE 1000000
- float add(float a,float b){return a+b;}
- float minus(float a,float b){return a-b;}
- float multiply(float a,float b){return a*b;}
- float divide(float a,float b){return a/b;}
- typedef float (*pf)(float,float);
- void switch_impl(float a,float b,char op)
- {
- float result=0.0;
- switch(op)
- {
- case '+':
- result=add(a,b);
- break;
- case '-':
- result=minus(a,b);
- break;
- case '*':
- result=multiply(a,b);
- break;
- case '/':
- result=divide(a,b);
- break;
- }
- }
- void switch_fp_impl(float a,float b,pf p)
- {
- float result=0.0;
- result=p(a,b);
- }
- int conversion(struct timeval tmp_time)
- {
- return tmp_time.tv_sec*COE+tmp_time.tv_usec;
- }
- int main()
- {
- int i=0;
- #if defined(UNIXENV)
- struct timeval start_point,end_point;
- gettimeofday(&start_point,NULL);
- #endif
- for(i=0;i<N;i++)
- {
- switch_impl(12.32,54.14,'-');
- }
- #if defined(UNIXENV)
- gettimeofday(&end_point,NULL);
- printf("check point 1:%d\n",conversion(end_point)-conversion(start_point));
- gettimeofday(&start_point,NULL);
- #endif
- for(i=0;i<N;i++)
- {
- switch_fp_impl(12.32,54.14,minus);
- }
- #if defined(UNIXENV)
- gettimeofday(&end_point,NULL);
- printf("check point 2:%d\n",conversion(end_point)-conversion(start_point));
- #endif
- return 0;
- }
下面是执行结果:
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22588
- check point 2:19407
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22656
- check point 2:19399
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22559
- check point 2:19380
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22181
- check point 2:19667
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22226
- check point 2:19813
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:22141
- check point 2:19893
- [lichao@sg01 replaceswitch]$ ./replaceswitch
- check point 1:21640
- check point 2:19745
从上面可以看出,使用函数指针:(一)在某种程度上简化程序的设计(二)可以提高效率。在这个例子中,使用函数指针可以提高10%的效率。
注意:以上代码在unix环境下实现的,如果要在windows下运行,可以稍微改下,把“#define UNIXENV”行删掉即可
3 C/C++函数指针的语法
从语法上讲,有两种不兼容的函数指针形式:
(1) 指向C语言函数和C++静态成员函数的函数指针
(2) 指向C++非静态成员函数的函数指针
不兼容的原因是因为在使用C++非静态成员函数的函数指针时,需要一个指向类的实例的this指针,而前一类不需要。
3.1 定义一个函数指针
指针是变量,所以函数指针也是变量,因此可以使用变量定义的方式来定义函数指针,对于普通的指针,可以这么定义:
int a=10;
int *pa=&a;
这里,pa是一个指向整型的指针,定义这个指针的形式为:
int * pa;
区别于定义非指针的普通变量的“形式”就是在类型中间和指针名称中间加了一个“*”,所以能够表达不同的“内容”。这种形式对于表达的内容是完备的,因为它说明了两点:(1)这是一个指针(2)这是一个指向整型变量的指针
以下给出三个函数指针定义的形式,第一个是C语言的函数指针,第二个和第三个是C++的函数指针的定义形式(都是指向非静态函数成员的函数指针):
int (*pFunction)(float,char,char)=NULL;
int (MyClass::*pMemberFunction)(float,char,char)=NULL;
int (MyClass::*pConstMemberFunction)(float,char,char) const=NULL;
我们先不管函数指针的定义形式,如果让我们自己来设计指向函数的函数指针的定义形式的话,我们会怎么设计?
首先,要记住一点的就是形式一定要具备完备性,能表达出我们所要表达的内容,即指向函数这个事实。我们知道普通变量指针可以指向对应类型的任何变量,同样函数指针也应该能够指向对应类型的任何变量。对应的函数类型靠什么来确定?这个我们可以想一下C++的函数重载靠什么来区分不同的函数?这里,函数类型是靠这几个方面来确定的:(1)函数的参数个数(2)函数的参数类型(3)函数的返回值类型。所以我们要设计一种形式,这种形式定义的函数指针能够准确的指向这种函数类型的任何函数。
在C语言中这种形式为:
返回类型 (*函数指针名称)(参数类型,参数类型,参数类型,…);
嗯,定义变量的形式显然不是我们通常见到的这种形式:
类型名称 变量名称;
但是,这也是为了表达函数这种相对复杂的语义而不得已采用的非一致表示形式的方法。因为定义的这个函数指针变量,能够明确的表达出它指向什么类型的函数,这个函数都有哪些类型的参数这些信息,确切的说,它是完备的。你可能会问为什么要加括号?形式上讲能不能更简洁点?不能,因为不加括号就会产生二义性:
返回类型 *函数指针名称(参数类型,参数类型,参数类型,…);
这样的定义形式定义了一个“返回类型为‘返回类型*’参数为(参数类型,参数类型,参数类型,…)的函数而不是函数指针了。
接下来,对于C++来说,下面这样的定义形式也就不难理解了(加上类名称是为了区分不同类中定义的相同名称的成员函数):
返回类型 (类名称::*函数成员名称)(参数类型,参数类型,参数类型,….)
3.2 函数的调用规则
一般来说,不用太关注这个问题。调用规则主要是指函数被调用的方式,常见的有_stdcall,_fastcall,_pascal,_cdecl等规则。不同的规则在参数压入堆栈的顺序是不同的,同时在有调用者清理压入堆栈的参数还是由被调用者清理压入堆栈的参数上也是不同的。一般来说,如果你没有显式的说明调用规则的话,编译器会统一按照_cdecl来处理。
3.3 给函数指针赋值和调用
给函数指针赋值,就是为函数指针指定一个函数名称。这个过程很简单,下面是两个例子:
int func1(float f,int a,int b){return f*a/b;}
int func2(float f,int a,int b){return f*a*b}
然后我们给函数指针pFunction赋值:
pFunction=func1;
pFunction=&func2;
上面这段代码说明了两个问题:(1)一个函数指针可以多次赋值(想想C++中的引用)(2)取地址符号是可选的,却是推荐使用的。
我们可以思考一下为什么取地址符号是可选的,在普通的指针变量赋值时,如上面所示,需要加取地址符号,而这里却是可选的?这是由于要同时考虑到两个因素(1)避免二义性(2)形式一致性。在普通指针赋值,需要加取地址符号是为了区别于将地址还是将内容赋给指针。而在函数赋值时没有这种考虑,因为这里的语义是清晰的,加上&符号是为了和普通指针变量一致---“因为一致的时候就不容易出错”。
最后我们来使用这个函数
pFunction(10.0,’a’,’b’);
(*pFunction)(10.0,’a’,’b’);
上面这两种使用函数指针调用函数的方式都是可以的,原因和上面一样。
下面来说明C++中的函数指针赋值和调用,这里说明非静态函数成员的情况,C++中规则要求的严格的多了。让我感觉C++就像函数指针的后爸一样,对函数指针要求特别死,或许是因为他有一个函数对象这个亲儿子。
在C++中,对于赋值,你必须要加“&”,而且你还必须再次之前已经定义好了一个类实例,取地址符号要操作于这个类实例的对应的函数成员上。在使用成员函数的指针调用成员函数时,你必须要加类实例的名称,然后再使用.*或者->*来使用成员函数指针。举例如下:
MyClass
{
public:
int func1(float f,char a,char b)
{
return f*a*b;
}
int func2(float f,char a,char b) const
{
return f*a/b;
}
}
首先来赋值:
MyClass mc;
pMemberFunction= &mc.func1; //必须要加取地址符号
pConstMemberFunction = &mc.func2;
接下来,调用函数:
(mc.*pMemberFunction)(10.0,’a’,’b’);
(mc.*pConstMemberFunction)(10.0,’a’,’b’);
我感觉,C++简直在虐待函数指针啊。
下面是一个完整的例子:
- /*
- *Author:Choas Lee
- *Date:2012-02-28
- */
- #include<stdio.h>
- float func1(float f,char a,char b)
- {
- printf("func1\n");
- return f*a/b;
- }
- float func2(float f,char a,char b)
- {
- printf("func2\n");
- return f*a*b;
- }
- class MyClass
- {
- public:
- MyClass(float f)
- {
- factor=f;
- }
- float func1(float f,char a,char b)
- {
- printf("MyClass::func1\n");
- return f*a/b*factor;
- }
- float func2(float f,char a,char b) const
- {
- printf("MyClass::func2\n");
- return f*a*b*factor;
- }
- private:
- float factor;
- };
- int main(int argc,char *argv[])
- {
- float (*pFunction)(float,char,char)=NULL;
- float (MyClass::*pMemberFunction)(float,char,char)=NULL;
- float (MyClass::*pConstMemberFunction)(float,char,char)const=NULL;
- float f=10.0;
- char a='a',b='b';
- float result;
- pFunction=func1;
- printf("pointer pFunction's address is:%x\n",pFunction);
- result=(*pFunction)(f,a,b);
- printf("result=%f\n",result);
- pFunction=&func2;
- printf("pointer pFunction's address is:%x\n",pFunction);
- result=pFunction(f,a,b);
- printf("result=%f\n",result);
- if(func1!=pFunction)
- printf("not equal.\n");
- pMemberFunction=&MyClass::func1;
- MyClass mc1(0.2);
- printf("pointer pMemberFunction's address is:%x\n",pMemberFunction);
- result=(mc1.*pMemberFunction)(f,a,b);
- printf("result=%f\n",result);
- pConstMemberFunction=&MyClass::func2;
- MyClass mc2(2);
- printf("pointer pConstMemberFunction's address is:%x\n",pConstMemberFunction);
- result=(mc2.*pConstMemberFunction)(f,a,b);
- printf("result=%f\n",result);
- return 0;
- }
运行结果为:
- pointer pFunction's address is:400882
- func1
- result=9.897959
- pointer pFunction's address is:400830
- func2
- result=95060.000000
- not equal.
- pointer pMemberFunction's address is:400952
- MyClass::func1
- result=1.979592
- pointer pConstMemberFunction's address is:4008f2
- MyClass::func2
- result=190120.000000
注意:上面的代码还说明了一点就是函数指针的一些基本操作,函数指针没有普通变量指针的算术操作,但是可以进行比较操作。如上面代码所示。
使用类的静态函数成员的函数指针和使用C语言的函数很类似,这里仅仅给出一个例子和其执行结果:
程序代码为:
- /*
- *Author:Chaos Lee
- *Date:2012-02-28
- */
- #include<iostream>
- class MyClass
- {
- public:
- static float plus(float a,float b)
- {
- return a+b;
- }
- };
- int main()
- {
- float result,a=10.0,b=10.0;
- float (*p)(float,float);
- p=&MyClass::plus;
- result=p(a,b);
- printf("result=%f\n",result);
- return 0;
- }
执行结果为:
- result=20.000000