这两天在研究C/C++的函数指针,找到一篇讲解比较详细的博客,内容有点多,但是讲解得比较详细,适合初学者。特转之:
使用函数指针可以设计出更优雅的程序,比如设计一个集群的通信框架的底层通信系统:首先将要每个消息的对应处理函数的指针保存映射表中(使用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语句的例子,为了能够比较出二者效率差异,所以在循环中进行了大量的计算。
1 /* 2 3 *Author:Choas Lee 4 5 *Date:2012-02-28 6 7 */ 8 9 #include<stdio.h> 10 #define UNIXEVN 11 12 #if defined(UNIXENV) 13 14 #include<sys/time.h> 15 16 #endif 17 18 #define N 1000000 19 20 #define COE 1000000 21 22 float add(float a,float b){return a+b;} 23 24 float minus(float a,float b){return a-b;} 25 26 float multiply(float a,float b){return a*b;} 27 28 float divide(float a,float b){return a/b;} 29 30 typedef float (*pf)(float,float); 31 32 void switch_impl(float a,float b,char op) 33 34 { 35 36 float result=0.0; 37 38 switch(op) 39 40 { 41 42 case '+': 43 44 result=add(a,b); 45 46 break; 47 48 case '-': 49 50 result=minus(a,b); 51 52 break; 53 54 case '*': 55 56 result=multiply(a,b); 57 58 break; 59 60 case '/': 61 62 result=divide(a,b); 63 64 break; 65 66 } 67 68 } 69 70 void switch_fp_impl(float a,float b,pf p) 71 72 { 73 74 float result=0.0; 75 76 result=p(a,b); 77 78 } 79 80 int conversion(struct timeval tmp_time) 81 82 { 83 84 return tmp_time.tv_sec*COE+tmp_time.tv_usec; 85 86 } 87 88 int main() 89 90 { 91 92 int i=0; 93 94 #if defined(UNIXENV) 95 96 struct timeval start_point,end_point; 97 98 99 100 gettimeofday(&start_point,NULL); 101 102 #endif 103 104 for(i=0;i<N;i++) 105 106 { 107 108 switch_impl(12.32,54.14,'-'); 109 110 } 111 112 #if defined(UNIXENV) 113 114 gettimeofday(&end_point,NULL); 115 116 printf("check point 1:%d\n",conversion(end_point)-conversion(start_point)); 117 118 119 120 gettimeofday(&start_point,NULL); 121 122 #endif 123 124 for(i=0;i<N;i++) 125 126 { 127 128 switch_fp_impl(12.32,54.14,minus); 129 130 } 131 132 #if defined(UNIXENV) 133 134 gettimeofday(&end_point,NULL); 135 136 printf("check point 2:%d\n",conversion(end_point)-conversion(start_point)); 137 138 #endif 139 140 return 0; 141 142 }