“函数指针”和“指针函数”是一对容易把人弄晕的概念,但我们只要把握好定语,倒也不难理解。这两个名词都是简称,“指针函数”是“返回值为指针的函数”,而“函数指针”则是“指向函数的指针”。这篇主要讲讲函数指针。

我们讲有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语言学习笔记(七) 函数指针_第1张图片

注:在《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);