摘要:这篇文章详细介绍C/C++的函数指针,请先看以下几个主题:使用函数指针定义新的类型、使用函数指针作为参数、使用函数指针作为返回值、使用函数指针作为回调函数、使用函数指针数组,使用类的静态函数成员的函数指针、使用类的普通函数成员的指针、定义函数指针数组类型、使用函数指针实现后绑定以及在结构体中定义函数指针。如果您对以上这几个主题都很了解,那么恭喜您,这篇文章不适合您啦~。在一些开源软件中,如Boost, Qt, lam-mpi中我们经常看到函数指针,本文目的是彻底搞定函数指针的语法和语义,至于怎样将函数指针应用到系统架构中不在此文的讨论范围中。各位看官,有砖拍砖啊~
无处不见的函数指针
使用函数指针可以设计出更优雅的程序,比如设计一个集群的通信框架的底层通信系统:首先将要每个消息的对应处理函数的指针保存映射表中(使用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++语言函数指针难学的主要原因是由于其形式上的定义过于复杂,但是在内容上我们一定要搞清楚函数的本质。函数的本质就是表达式的抽象,它在内存中对应的数据结构为堆栈帧,它表示一段连续指令序列,这段连续指令序列在内存中有一个确定的起始地址,它执行时一般需要传入参数,执行结束后会返回一个参数。和函数相关的,应该大致就是这些内容吧。
函数指针简单介绍
什么是函数指针
函数指针是一个指向函数的指针(呃,貌似是废话),函数指针表示一个函数的入口地址。使用函数指针的好处就是在处理“在运行时根据数据的具体状态来选择相应的处理方式”这种需求时更加灵活。
一个简单的例子
下面是一个简单的使用函数指针取代switch-case
语句的例子,为了能够比较出二者效率差异,所以在循环中进行了大量的计算。
#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”
行删掉即可。
C/C++函数指针的语法
从语法上讲,有两种不兼容的函数指针形式:
(1) 指向C语言函数和C++静态成员函数的函数指针
(2) 指向C++非静态成员函数的函数指针
不兼容的原因是因为在使用C++非静态成员函数的函数指针时,需要一个指向类的实例的this
指针,而前一类不需要。
定义一个函数指针
指针是变量,所以函数指针也是变量,因此可以使用变量定义的方式来定义函数指针,对于普通的指针,可以这么定义:
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++来说,下面这样的定义形式也就不难理解了(加上类名称是为了区分不同类中定义的相同名称的成员函数):
返回类型 (类名称::*函数成员名称)(参数类型,参数类型,参数类型,….)
函数的调用规则
一般来说,不用太关注这个问题。调用规则主要是指函数被调用的方式,常见的有_stdcall,_fastcall,_pascal,_cdecl
等规则。不同的规则在参数压入堆栈的顺序是不同的,同时在有调用者清理压入堆栈的参数还是由被调用者清理压入堆栈的参数上也是不同的。一般来说,如果你没有显式的说明调用规则的话,编译器会统一按照_cdecl
来处理。
给函数指针赋值和调用
给函数指针赋值,就是为函数指针指定一个函数名称。这个过程很简单,下面是两个例子:
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++中,对于赋值,你必须要加“&”,(注:这里原作者说的并不准确,对于类的成员函数,也可以不用取地址符,但对于类的非静态成员函数,必须定义好类实例。原因是非静态成员函数一般要处理对象的非静态数据成员,这就需要传递this指针,所以必须实例化类为对象。)而且你还必须再次之前已经定义好了一个类实例,取地址符号要操作于这个类实例的对应的函数成员上。在使用成员函数的指针调用成员函数时,你必须要加类实例的名称,然后再使用.或者->来使用成员函数指针。举例如下:
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; //必须要加取地址符号 ps.其实并不用 0.0
pConstMemberFunction = &mc.func2;
接下来,调用函数:
(mc.*pMemberFunction)(10.0,’a’,’b’); //实例化!
(mc.*pConstMemberFunction)(10.0,’a’,’b’);
我感觉,C++简直在虐待函数指针啊。
下面是一个完整的例子:
#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语言的函数很类似,这里仅仅给出一个例子和其执行结果:
程序代码为:
#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
函数指针作为参数
如果你已经明白了函数的参数机制,而且完全理解并实践了上一节的内容,这一节其实是很简单的。只需要在函数的参数列表中,声明一个函数指针类型的参数即可,然后再调用的时候传给它一个实参就可以了。你可以这么想象,就是把函数指针的赋值语句的等号换成了形参和实参结合的模式就行。
下面给一个简单的例子:
#include<stdio.h>
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;}
int pass_func_pointer(float (*pFunction)(float a,float b))
{
float result=pFunction(10.0,12.0);
printf("result=%f\n",result);
}
int main()
{
pass_func_pointer(add);
pass_func_pointer(minus);
pass_func_pointer(multiply);
pass_func_pointer(divide);
return 0;
}
输出结果为:
result=22.000000
result=-2.000000
result=120.000000
result=0.833333
使用函数指针作为返回值
函数指针可以作为返回值。我们先类比的思考一下,如果说整型可以作为返回值,你会怎么声明函数?嗯,应该是下面这个样子的:
int func(){}
整数对应的类型为int
。同样再类比以下,如果说整型指针可以作为返回值,你会怎么声明?嗯,这个貌似难度也不大:
int * func(){}
好吧,现在说函数指针如果可以作为返回值,你该怎么声明?首先要保证的一点就是返回的函数指针的类型必须是能够明显的表达在这个函数的声明或者定义形式中的,也就是说在这个形式中,要能够包含函数指针所对应的能够确定函数类型的信息:这个函数类型的返回值类型,这个函数类型的参数个数,这个函数类型的参数类型。
现在我们在类比一次,如果要返回浮点型指针,那么返回类型应该表达为:
float *
如果要函数指针对应的函数是返回值为浮点型,带有两个参数,两个参数都是浮点型,那么返回类型应该表达为下面的表达形式:
float (*)(float ,float )
嗯,没办法,函数的语义比较复杂,对应的表现就是形式的复杂性了。对于返回为浮点型指针的情况,定义的函数的名称放在“float *”
的后面,而对于返回为上面类型的函数指针的话,定义的函数就要放在“(*)”
这个括号中的*
的后面了。
所以对于以下形式:
float (* func(char op) ) (float ,float)
其具体含义就是,声明了这样一个函数:
- 名称为func,其参数的个数为1个;
- 其各个参数的类型为:op—char;
- 其返回变量(函数指针)类型为:
float(*)(float,float)
再次强调:函数指针时变量哦。
到了这里之后,我们再来分析一下unix的系统调用函数signal的定义形式:
void (*signal(int signo,void (*func)(int)))(int);
其具体含义为就是,声明了这样一个函数:
- 其函数名称为:
signal
- 其参数个数为:
2
- 其各个参数的类型为:
signo--int, func— void (*)(int)
- 其返回的变量(函数指针)的类型为:
void(*)(int)
上面这个函数比较经典,有一个参数类型为函数指针,返回值还是函数指针。
哦,我的天,如果你一步一步看到这里了,就快大功告成啦。嘿嘿,接下来看一个例子:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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;}
float(* FunctionMap(char op) )(float,float)
{
switch(op)
{
case '+':
return add;
break;
case '-':
return minus;
break;
case '*':
return multiply;
break;
case '\\':
return divide;
break;
default:
exit(1);
}
}
int main()
{
float a=10,b=5;
char ops[]={'+','-','*','\\'};
int len=strlen(ops);
int i=0;
float (*returned_function_pointer)(float,float);
for(i=0;i<len;i++)
{
returned_function_pointer=FunctionMap(ops[i]);
printf("the result caculated by the operator %c is %f\n",ops[i],returned_function_pointer(a,b));
}
return 0;
}
计算的结果为:
the result caculated by the operator + is 15.000000
the result caculated by the operator - is 5.000000
the result caculated by the operator * is 50.000000
the result caculated by the operator \ is 2.000000
使用函数指针数组
函数指针有意思的地方在于,它使用从0到n-1这个n个连续的整数下标直接映射到函数上。
和前面一样,我们也是类比着定义普通指针数组来定义函数指针数组。首先,考虑一个浮点数指针数组,数组的长度为10.我们都知道用下面的形式来定义:
float * pFloatArray[10];
从形式上分析,用中括号明确了是定义指针变量还是定义指针数组这个语义。用数字10明确了这个数组能容纳多少个函数指针这个语义。形式上看,中括号是紧接在指针名称的后面再中括号里面是一个需要在编译时期间就能够确定的常数。
现在我们来类比函数指针数组的定义,定义一个指向函数指针类型为:float (*)(float,float)
的函数指针数组,数组长度为10。正确的形式为:
float(* pFunctionArray[10])(float,float);
从形式上看,这种定义方式和定义普通指针的定义方式是一致的:都是在指针名称后面紧接着一个中括号,然后里面是一个编译期间能够确定的常数。这种形式上的一致性,可以方便我们对形式的记忆,进而达到对内容的理解。
下面是一个例子程序:
#include<stdio.h>
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;}
int main()
{
float(*func_pointers[4])(float,float)={add,minus,multiply,divide};
int i=0;
float a=10.0,b=5.0;
for(i=0;i<4;i++)
{
printf("result is %f\n",func_pointers[i](a,b));
}
return 0;
}
以下为对应的运行结果:
result is 15.000000
result is 5.000000
result is 50.000000
result is 2.000000
使用typedef
从哲学角度讲,形式过于复杂的话,还是抽象的层次太低。如果我们使用多层次的抽象,这样最上层的表示就会简化很多。这就是引入typedef
的原因,使用typedef
可以简化函数指针的定义,因为typedef
可以定义新的类型:
同样,在使用typedef
定义函数指针类型的时候,也和普通的使用typedef
引入新类型的方式不一样。我们和前面一样对照着普通的定义方式来学习:
typedef int bool;
这在C语言中很常用,由于C语言中没有bool
类型,这样定义之后可以从形式上引入一个bool
类型,提高代码可读性。所以形式为:
typedef 已知类型 新类型;
现在我们要将float (*)(float,float)
类型声明为一种新类型,按照上面的方式,貌似为:typedef float(*)(float,float) fpType;
然而,前面的经验告诉我们应该这样定义啊:
typedef float(*fpType)(float,float);
这样我们就可以用fpType
来表示float (*)(float,float)
这种类型了。所以定义一个新的指向float (*)(float,float)
类型的指针变量的时候,我们就可以采用下面这种形式了:
fpType pFunction;
在定义函数指针数组的时候可以这样定义:
fpType pFunctions[10];
在定义函数指针类型参数时可以这样定义:
void func(fpType pFunction);
在定义函数指针类型的返回值时可以这样定义:
fpType func(int a);
现在我们再来看一下,unix
中的那个signal
函数,其形式为:
void (*signal(int signo,void (*func)(int)))(int);
现在我们定义一个类型为:
typedef void (*pSgnType)(int);
这样上面的函数就能表达为:
pSgnType signal(int signo,pSgnType func);
这样是不是看起来清爽多了。
其实上面的signal
函数也能这样定义:
首先引入新类型:
typedef void SgnType(int);
然后signal
函数的声明改为:
SgnType *signal(int signo,SgnType *func);
按照前面对这些形式的解释,理解这个应该没难度~~
现在在引入最后一个例子,关于使用typedef
来简化函数指针定义的:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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 (*pArithmeticOperations)(float,float);
typedef float ArithmeticOperations(float,float);
int main()
{
pArithmeticOperations pao=add;
pArithmeticOperations paos[4]={add,minus,multiply,divide};
ArithmeticOperations *ao=add;
ArithmeticOperations *aos[4]={add,minus,multiply,divide};
float a=10.0,b=5.0;
float result=0.0;
int i=0;
result=pao(a,b);
printf("the result of pao is %f\n",result);
printf("the results of paos are:\n");
for(i=0;i<4;i++)
{
result=paos[i](a,b);
printf("result=%f\n",result);
}
result=ao(a,b);
printf("\n\nthe result of ao is :%f\n",result);
printf("the results of aos are:\n");
for(i=0;i<4;i++)
{
result=aos[i](a,b);
printf("result=%f\n",result);
}
return 0;
}
输出结果为:
result=15.000000
result=5.000000
result=50.000000
result=2.000000
the result of ao is :15.000000
the results of aos are:
result=15.000000
result=5.000000
result=50.000000
result=2.000000
修正原文错误,阅读原文请点击: