C语言教程-13_4-函数指针
前置知识:
- 函数
- 指针
函数指针
C语言中,函数也是一个可寻址对象,我们也可以获取其地址——换句话说,函数代码被存储于内存中的某一个地方,并且可以根据其指针来访问.
调用该函数时,程序就会跳转到该函数的地址,运行此处的代码,也即调用了该函数.
显然,我们可以使用一种特殊的指针来存储一个函数的地址(指针),并且完全可以使用该指针来进行访问.
这就是函数指针,或者说"指向函数的指针".
声明一个函数指针
我们可以如此声明一个函数指针:
#include <stdio.h>
/* func函数用于返回int参数2倍 */
int func(int i){
return i*2;
}
int main() {
int (*p)(int) = func; // p是一个函数指针,初始化为指向func函数
printf("p: 0x%p\n", p); // 输出p的值
printf("&func: 0x%p\n", &func); // 输出func函数的地址
printf("(*p)(3): %d\n",(*p)(3)); // 使用p指针调用func函数
printf("func(4): %d\n",(*p)(4)); // 调用func函数
return 0;
}
考虑声明 int (*p)(int):
首先从标识符p开始,有一对()约束*p,指出p是一个指针;
然后在(*p)外的int (int)代表一个函数类型,此函数接受一个int参数,并返回一个int值;
因此推导出,p是一个指针,可以指向int (int)类型的函数,亦即p是一个函数指针.
上面代码的运行结果如下:
可以看出,指针p的值(p指向的内存)就是func的地址.
函数指针的类型
前一个例子中,函数指针p指向的函数的类型是int (int),它代表了"一类"函数,而不是具体的一个函数,考虑下面代码:
#include <stdio.h>
/* func1函数用于返回int参数2倍 */
int func1(int i) {
return i * 2;
}
/* func函数用于返回int参数3倍 */
int func2(int i) {
return i * 3;
}
int main() {
int (*p)(int) = func1; // p是一个函数指针,初始化为NULL
p = func1; // p指向func1函数
printf("/*当前p指向func1函数*/\n");
printf("p: %p\n", p);
printf("&func1: %p\n", &func1);
printf("(*p)(2): %d\n", (*p)(2));
printf("func1(3): %d\n", func1(3));
p=func2; // p指向func2函数
printf("\n/*当前p指向func2函数*/\n");
printf("p: %p\n", p);
printf("&func2: %p\n", &func2);
printf("(*p)(2): %d\n", (*p)(2));
printf("func2(3): %d\n", func2(3));
return 0;
}
运行结果如下:
显然,func1和func2的类型完全相同,均为int (int),但是他们却是完全不同的2个函数,p指针可以分别指向他们,并进行调用.
另一方面,复习指针的内容:指针变量只能被赋值(指向)为指针变量所能指向的类型的地址,或者是能够被隐式转换为这种类型的地址.
和其他所有指针一样,函数指针只能指向自身指向的类型(即特定的一种函数类型)的地址.
但是需要注意,不同类型的函数指针是不能够互相转换的,也就是说,int (*p)(int)只能指向int (int)类型的函数,而不能指向例如int (double)类型的函数:
#include <stdio.h>
/* 2个不同类型的函数 */
int func1(int i) {
return i * 2;
}
int func2(double i) {
return (int)i;
}
int main() {
int (*p)(int) = func1;
// 下面这行代码会报错:
// invalid conversion from 'int (*)(double)' to 'int (*)(int)'
// p = func2;
printf("p: %p\n", p);
return 0;
}
C语言中,不仅仅是参数类型,包括返回值类型,参数个数不同,都意味着他们不是相同类型的函数!
注: C语言没有函数重载,而且即使是C++的函数重载也不允许有这种指针类型转换
函数名的本质
C语言在处理函数指针和函数调用这方面有一些很有意思的特性,先看下面的代码,有可能让你懵逼:
#include <stdio.h>
int func(int i){
return i*2;
}
int main() {
int (*p)(int) = func;
printf(" p: %p\n", p);
printf(" *p: %p\n", *p);
printf(" func: %p\n", func);
printf("*func: %p\n", *func);
printf("&func: %p\n", &func);
return 0;
}
令人迷惑的是,每一行的输出值都是一样的:
我们可以将其反汇编查看汇编代码,结果发现每一步的参数都是完全一样的:
也就是说,无论是对func做*运算,还是&运算,还是直接对函数名func求值,得到的结果甚至汇编代码都是完全一致的,函数指针p也是如此.
事实上,函数名被使用时总是被编译器转换为函数指针,因此,诸如&func这样手动加上&运算符只不过是显式地说明了编译器将隐式执行的任务.
因此,我们通常会这样简化对函数指针的赋值:
#include <stdio.h>
int func(int i) {
return i * 2;
}
int main() {
int (*p)(int) = func; // 直接把函数名赋值给指针,由编译器自动转换
printf("p: %p\n", p);
return 0;
}
而同时,使用函数指针进行函数调用的代码也可以如此简化:
// 简化使用指针进行函数调用
#include <stdio.h>
int func(int i) {
return i * 2;
}
int main() {
int (*p)(int) = func; // 直接把函数名赋值给指针,由编译器自动转换
printf("p(2): %d\n", p(2)); // 不需要(*p)(2),因为*p是函数,编译器仍然会将其重新转换为函数指针
// 下面的代码没有问题,但是一步解引用看起来多此一举
printf("(*p)(2): %d\n", (*p)(2));
return 0;
}
另外,尽管从上面的角度分析来看,这种编译器负责的"转换"是必然发生的,但是事实并非如此:
就拿上面这段代码("简化使用指针进行函数调用")来看,编译器甚至可能直接把这个指针变量p优化掉:
指针变量p呢?我不知道……你知道吗(手动滑稽)
所以从实际优化的角度来看,简单的函数指针使用甚至会直接被优化掉,更别提什么"函数名的转换"了,统统直接用lea func指令获取地址就完事了……
总结:实际使用过程中一旦用一个函数指针p来指向某个函数f,那么干脆直接把p当成函数f的一个别名也无伤大雅.唯一的区别是,p是一个指针,可以改变它的指向.
函数指针数组
这个很简单,前面讲过指针数组,函数指针数组就是一种指针数组,只不过这个数组中的每个元素都是一个函数指针而已.
简单举个例子,注意声明的写法:
#include <stdio.h>
int add(int a, int b) {
printf("add: %d + %d", a, b);
return a + b;
}
int sub(int a, int b) {
printf("sub: %d - %d", a, b);
return a - b;
}
int mul(int a, int b) {
printf("mul: %d * %d", a, b);
return a * b;
}
int div(int a, int b) {
printf("div: %d / %d", a, b);
return a / b;
}
int main() {
/* p是一个数组,每个元素是一个指针,指针指向的类型是int (int,int)函数 */
int (*p[4])(int, int) = { add, sub, mul, div }; // 使用4个函数指针来初始化数组
int x, y;
scanf("%d %d", &x, &y);
for (int i = 0; i < 4; i++) {
printf(" = %d\n", p[i](x, y));
}
return 0;
}
以8 2作为x,y的值运行,结果如下:
函数指针数组可以存储一系列类似功能(接口)的函数的指针,适当地使用可以大大简化程序,例如实现一个功能菜单的选择.
——WAHAHA 2024.1.23

浙公网安备 33010602011771号