函数指针属于 C 语言的进阶内容之一,前两天研究 tinyhttpd 时,碰到了函数指针,产生了一些疑问...
普通指针 在研究函数指针之前,先回顾一下普通指针,先看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdio.h> struct point { int x, y; }; int main (int argc, char const *argv[]) { int a = 10 , *pa = &a; printf ("*pa = %d, a = %d\n" , *pa, a); float f = 1.0 , *pf = &f; printf ("*pf = %f, f = %f\n" , *pf, f); struct point A , *pA = &A; A.x = 3 , A.y = 3 ; printf ("A.x = %d, A.y = %d\n" , A.x, A.y); printf ("pA->x = %d, pA->y = %d\n" , pA->x, pA->y); printf ("*pA.x = %d, *pA.y = %d\n" , (*pA).x, (*pA).y); return 0 ; }
从输出结果可以看出,不管是普通变量的指针,还是结构体的指针,都是普通的指针变量,而指针的本质就是保存地址的变量,通过这个地址可以找到对应的变量 。利用&符号,可以得到变量的地址从而赋值给指针变量,这样这个指针变量就指向了使用&运算符的变量。
函数指针 现在,回到对函数指针的思考上,首先应该要认识的是:函数指针的本质还是指针 ,它与普通指针没有什么差别,还是指针变量 ,只不过这个指针变量指向的是函数,就跟不同类型的指针变量指向对应类型的变量一样。 通过上面的代码,我们知道&符号可以取出一个变量的地址,那函数的地址是不是也可以这样取出呢?写个程序测试一下:
test1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> void fun () { }int main (int argc, char const *argv[]) { printf ("&fun = %p\n" , &fun); printf ("sizeof(&fun) = %d\n" , sizeof (&fun)); return 0 ; }
可以发现使用&符号的确可以输出函数的地址,同时使用sizeof关键字得到了“取出的地址”的大小。按照前面的思路,我们知道这是一个指针,但这是个什么类型的指针,不知道,为什么是 4,也不知道,但我们可以再多写点代码验证一下思路:
test2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> void fun () { }int main (int argc, char const *argv[]) { printf ("&fun = %p\n" , &fun); printf ("sizeof(&fun) = %d\n" , sizeof (&fun)); int i = 10 , *pi = &i; printf ("sizeof(&i) = %d\n" , sizeof (&i)); printf ("*pi = %d, sizeof(pi) = %d\n" , *pi, sizeof (pi)); return 0 ; }
现在,可以很清楚的确定,利用&取出的函数的地址与指针变量是一样,它们都是 4 个字节的大小,那么也就是说函数指针也是一种指针变量,这与前面的思路是一致的。
了解函数指针大概是什么样子的后,肯定会冒出这样的问题:怎么使用函数指针变量指向某个函数呢?换句话说,就是如何像使用int i, *a = &i;这样,声明某个指针变量并指向某个函数呢?
显然,这个问题 C 语言肯定是有规定的,函数指针的声明方式与函数声明是类似的,只是需要加上*和()。同样,我们用代码说话:
test3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> int fun (int a) { printf ("In fun, a = %d\n" , a); return a; } int main (int argc, char const *argv[]) { int (*pf)(int ) = &fun, a = 10 ; pf(a); return 0 ; }
在上面的代码中,声明了一个函数指针变量pf指向函数fun,然后通过pf指针,调用了函数fun,输出a的值。相比函数的声明而言,有两个特点:
省略了参数名,但不能省略参数类型,且必须与被指向的函数参数类型、数量一致
需要用*和()讲指针名括起来
按照上面的思路,可以知道函数指针的声明方式是[return-type] (*pointer-name)(parameter-list)。
现在,我们在上面的代码中尝试将函数指针作为函数的参数使用:
test4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> int fun (int a) { printf ("In fun, a = %d\n" , a); return a; } void func (int (*pf)(int )) { int a = 10 ; pf(a); } int main (int argc, char const *argv[]) { func(&fun); return 0 ; }
test3 和 test4 两段代码的输出结果是完全一致的,不同的是在 test4 中,直接使用函数指针作为另一函数的参数,然后在另一函数中通过函数指针调用这个函数。可以发现,函数指针作为函数参数的写法与其声明没有太大差异。
数组指针 现在我们再回顾一下数组指针的用法,先看下面的代码:
test5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <stdio.h> void fun (int *pa) { printf ("*pa = %d\n" , *pa); printf ("sizeof(pa) = %d\n" , sizeof (pa)); } int main (int argc, char const *argv[]) { int a[5 ] = {1 , 2 , 3 , 4 , 5 }; fun(a); printf ("sizeof(a) = %d\n" , sizeof (a)); int (*parr)[5 ] = &a; printf ("a = %p, &a = %p\n" , a, &a); printf ("parr = %p\n" , parr); printf ("*parr = %d\n" , *parr); printf ("sizeof(&a) = %d\n" , sizeof (&a)); printf ("sizeof(parr) = %d\n" , sizeof (parr)); return 0 ; }
我们知道数组名就表示数组的首地址,所以可以将数组名直接作为函数的参数,从而传入数组的地址,但数组名和指向数组的指针是两个完全不同的东西,这一点可以通过sizeof关键字看出,也就是说parr跟&a是同一类事物——二者都是数组指针,对应的它的类型声明才是数组指针的声明,而sizeof则认为a是一个数组,所以sizeof(a) = 20。从这里也可以看出,数组指针的声明与函数指针类似,有两个条件需要指定:
数组的类型
数组的维度
不过,实际上,C 语言存在隐式类型转换(还有兼容老版本写法之类的历史遗留原因等),所以有时候写法不严格,编译也可以通过,但会产生 warnings,总之尽量保持类型一致,不要出现 warnings。 但这里有意思的是,数组名作为地址可以直接作为指针传给函数使用,那函数名呢?当然也可以了~ 那么,test4 的代码可以改成:
test4_1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> int fun (int a) { printf ("In fun, a = %d\n" , a); return a; } void func (int pf(int )) { int a = 10 ; pf(a); } int main (int argc, char const *argv[]) { func(fun); return 0 ; }
在上面的代码中,有两个改动的地方:
函数指针做参数的函数声明中,去掉了*
直接将函数名作为参数传入另一个函数中,去掉了&
虽然这两处改动都不影响使用,但是读起来容易引起混淆,语义不明确,这应该是某种“历史遗留写法”,最好不要这样写。 实际上,我们可以通过sizeof来观察一下函数名与函数指针的差异,再添几行代码:
test6 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> void func () { }int main (int argc, char const *argv[]) { printf ("sizeof(func) = %d, sizeof(&func) = %d\n" , sizeof (func), sizeof (&func)); return 0 ; }
显然,与数组类似,函数名与函数指针也是两个不同的东西。
指针数组 接下来,在看看指针数组(反着叫又是另外一种东西了😂)的用法,直接看下面的代码:
test7 1 2 3 4 5 6 7 8 #include <stdio.h> int main (int argc, char const *argv[]) { int a = 10 , b = 20 ; int *parr[2 ] = {&a, &b}; printf ("%d, %d\n" , *parr[0 ], *parr[1 ]); return 0 ; }
其实就是在指针后面加上了[]符号,对于其他类型也是一样的用法。
函数指针数组 说完了函数指针也数组指针的区别,见到了指针数组的用法,接下来,再来看看如何使用函数指针数组。 假设我们要计算两个整数的四则运算结果,为了使用到函数指针数组,先定义四个函数,然后放到函数指针数组中,再利用函数指针数组逐个调用函数完成计算。 按照这样的思路,可以写出以下代码:
test8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> int add (int a, int b) { return a + b; } int minus (int a, int b) { return a - b; } int mult (int a, int b) { return a * b; } int div (int a, int b) { return a / b; } int main (int argc, char const *argv[]) { int a = 10 , b = 10 ; int (*pfun[4 ])(int , int ) = {add, minus, mult, div}; for (int i = 0 ; i < 4 ; i++) printf ("pfun[%d](a, b) = %d\n" , i, pfun[i](a, b)); return 0 ; }
实际上,函数指针数组也就是在函数指针中间加上[],这与指针数组是类似的。但是要注意的是,函数指针数组只能保存“同一类”函数指针,而“同一类”在这里是指返回类型和参数列表需要一致。
总结 虽然这篇文章题目叫做函数指针,但是目前我们一共学习了五种不同的与指针相关的概念和用法,分别是:
普通指针
数组指针
函数指针
指针数组
函数指针数组
同时,还认识到了数组名与数组指针、函数名与函数指针的区别。这些都只是指针用法的冰山一角,指针还有很多其他的用法,比如通过指针在函数中修改实参的值,或者是在 C++ 中用父类指针指向子类对象,调用子类方法,从而实现动态多态等等。 所以,学习之路还很漫长啊~😪