“函数指针”和“指针函数”是一对容易把人弄晕的概念,但我们只要把握好定语,倒也不难理解。这两个名词都是简称,“指针函数”是“返回值为指针的函数”,而“函数指针”则是“指向函数的指针”。这篇主要讲讲函数指针。
我们讲有int 指针,char指针,它们都是一个指针指向这个变量的实际地址。而C语言在编译函数的时候每个函数会有一个入口地址,当我们用一个指针指向这个入口地址,它就称为函数指针。有了这样一个指针之后,可以通过访问这个指针来调用该函数。从这个角度看,实际上函数指针和基本类型指针定义是一样的,只是指向的对象不同而已。那么,来看看不同的函数指针吧。
新手场:指向函数的简单指针
int add(int a, int b){ return a+b; } int sub(int a, int b){ return a-b; } int main(int argc, char** argv){ int (*p)(int, int)=NULL; p = add; PRTINT( p(2,3) ); //输出5 p = sub; PRTINT( p(3,1) ); //输出2 return 0; }
说明:上面的PRTINT是一个输出宏,实际上就是printf函数,只是为了方便观察指针的使用。可以看出,函数指针的定义方式是 返回类型 (*指针名)([参数列表])。注意:
函数指针和指向函数的返回值的类型和参数都必须严格一致;
定义指针的形式还可以写作 int (*p)(int a, int b); 形参的命名没有影响;
给指针赋值的形式还可以写作 p = &add; 两种方式没有本质区别。
调用指针的形式还可以写作 (*p)(2,3)。
进阶场:复杂函数指针
*(int*)&p ----这是什么?
看看这段代码:
void fun(){ printf("call fun().\n"); } int main(int argc, char** argv){ void (*p)(); *(int*)&p = (int)&fun; //效果等价于 p = fun; (*p)(); return 0; }
虽然注释里已经“剧透”了,但是初次看到 *(int *)&p = (int)&fun; 这个表达式的朋友恐怕多半会头疼。对付这种复杂表达式,别急,一点点去啃,总会看懂的。
首先看看等号右边。如前所述,函数名本身实际上就可以代表一个地址,&fun也是这个地址。那么(int)&fun表示把这个地址转换成int 类型。
再看看前一个表达式: void (*p)();这是一个简单的函数指针。表示一个返回值为void、参数列表为空的函数指针(实际上就是fun的样式)。
&p表示取指针变量p本身的地址,这是一个32位常数(在32bit系统下)。
(int *)&p表示把取到的这个地址强制转换成int类型的指针。
那么 *(int *)&p = (int)&fun; 就是把这个fun()函数的地址赋给p了。所以实际上这行代码的效果等效于 p = fun; 。
从这个例子也可以看出,函数指针也是指针,完全没有什么特殊性,一般指针能做的操作它也能做。
(* (void (*)()) 0)();这又是什么?
这是《C陷阱与缺陷》里的一个经典例子。下面就来看看这句代码什么意思:
第一步: void (*)().这可以看出是一个函数指针,这种函数的返回值为void,参数列表也为空。注意,这个指针没有名字,也许是最令人困惑的地方。
第二步:(void (*)())0.这是把0强制转换成这种类型指针。0是一个地址,也就是说这里假定函数起始地址在0点处。
第三步:(* (void (*)())0)().看得出来这一步与前一步仅仅多出一个(*)().因此,这是函数调用。
虽然层层抽丝最终能看出这行代码的真面目,但实际上这句代码是无法直接执行的,不信的话你可以放在main函数里去试试。这是由于0地址的原因造成的。我们完全可以测试一下,使用以下代码:
void fun(){ printf("call fun().\n"); } int main(int argc, char** argv){ (* (void (*) () ) 0x401352)(); // void (*p)() = fun; // (*p)(); return 0; }
我使用的是CodeBlocks,先使用注释里的代码,并在Watch窗口查看到函数的实际地址值,我这里是0x401352。那么把上面代码中的0替换成这个地址,把后两句关掉,结果显示调用到了fun().
注:在《C陷阱与缺陷》原书中,这句代码的执行环境是一种微处理器。机器启动时硬件会调用首地址为0位置的程序。而这句代码实际上就是为了在开发的时候模拟开机启动时的情形。因此,在我们自己的机器上是无法直接用0地址的。
还有更为复杂的函数指针形式,这里不一一列举,但只要牢记方法就不用怕:先找到核心最像简单函数指针的部分,再一点点展开慢慢分析。这就好比孙猴子的火眼金睛,任你小妖精怎么换马甲,总会被咱看出破绽,是不是?
函数指针数组
现在我们可以看穿函数指针的马甲了,比如下面这个就是一个函数指针:
char * (*pf)(char *p);
考虑数组的概念,把相同类型的元素放在一起就可以组成数组。那么我们把函数指针放在一个数组里是不是就成了下面这样:
char *(*pf[3])(char *p);
这……星号好多呀……嗯,它是一个长度为3的数组。数组的每个元素都是一个函数指针。每个函数指针指向的函数返回值类型都是char*,形参列表都只有一个char*参数。喏,这样就理顺了,最重要的是它是一个数组。下面的代码演示了这种数组的使用:
char * fun1(char *p){ printf("%s\n", p); return p; } char * fun2(char *p){ printf("%s\n", p); return p; } int main(int argc, char **argv){ char* (*pa[2])(char*); pa[0] = fun1; //可以直接用函数名 pa[1] = &fun2; //也可以加上&符号 pa[0]("fun1"); pa[1]("fun2"); return 0; }
指向函数指针数组的指针
这个更拗口了。不过,只要我们了解了函数指针数组,则指向函数指针数组的指针也不是不能理解。函数指针数组的指针用起来跟其他指针没什么区别,只不过调试难度可能会增加。下面的代码定义了一个指向函数指针数组的指针,有兴趣的同学可以看看,这里就不赘述了。
char * (*(*pf)[3])(char *p);