函数指针
函数指针是围绕指针相关内容中举足轻重的一个话题。使用函数就要被加载到内存中,所以就会在内存中占有地址,所以就出现了函数指针。
函数指针是指向函数的指针变量。 因此“函数指针”本身应是指针变量,该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
C语言在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
设定一个函数:
int Func() {
printf("hehe\n");
return 10;
}
之后选择十六进制打印地址的方式打印以下内容,查看结果:
printf("%p\n", Func); //函数名
printf("%p\n", &Func); //函数名取地址
printf("%p\n", Func()); //函数名 + ()
printf("%p\n", *Func); //函数名解引用
printf("%p\n", (*Func)()); //函数名解引用 + ()
运行结果为:
-
Func
:只是数组名,没有加()
,它就是一个函数指针。
按照地址的打印形式输出了函数在内存中的地址。 (即第一行) -
&Func
:在此情况下,&Func
和上一种情况Func
完全等价,都是也打印了函数的地址 。(即第二行)
【注】:这里不能像之前操作数组时的arr + 1
与&arr + 1
来区分二者,因为对于函数指针加减整数没有什么实际意义,不太科学。
-
Func()
:函数名加上()
,就是函数调用了,所以先执行了函数内部的函数,打印了内容,之后对返回值进行了打印。 (即第三、四行)
【()
为函数调用操作符】 -
*Func
:函数名解引用,即函数指针解引用,得到的就是一个函数,输出了函数的地址,和首部的内容相同都是函数地址。 (即第五行) -
(*Func)()
:函数名解引用得到一个函数,在加上函数调用操作符()
,进行函数调用,此时和Func()
等效。(即第六、七行)
所以 *Func 和 (*Func)() 都是函数调用,作用也是完全一致的。
【 ! 】这里需要注意:
Func
的返回的是一个void*
类型的参数,前面说到void*
不可以解引用,因为不知道指针变量的大小,提领时不知道该读取多少个字节的数据。但在函数指针这里是适用的。
那么应该使用什么样类型的变量存储这个函数指针变量?
// 方法 一
void *p = Func;
- 这样是可以的,因为二者的类型都为
void *
,同类型的存储是适合的,但更推荐下面这种更科学的函数指针变量的类型进行存储。
// 方法 二
int (*p)() = Func;
此时定义了一个指针变量p,p的类型是int(*)()
的函数指针类型。
注意与数组指针int(*)[]
加以区分,二者很相似。函数指针类型之后是圆括号,而数组指针之后是方括号。
所以建议使用时进行类型重定义,使代码简单直观,如下:
typedef int(*T)(); //此时定义了一个类型的别名 T
T p2 = Func; //定义函数指针变量p2
这样的书写就相比于上面方法一的风格更加清晰明了,推荐使用。
如果是有传参的函数,则如此书写:
int Add(int x,int y){
return x + y;
}
int(*p3)(int,int) = Add; //此时p3指向Add函数
此时p3也是一个函数指针变量,类型是int (*)(int,int)
,它的两个形参可以省略,但是形参的类型名是不可省的。
int(*T)()
类型的p2
,与int (*)(int,int)
类型的p3
就不是同一种类型的变量了。如果进行
p2 = p3;
操作,弱类型的C语言允许这样的强制类型转换,但强类型的C++中是不允许的。
如果执行了上行代码,再执行p2()
就会出现未定义行为
,因为p3
是要传递参数的,而p2
是不传参的,所以属于未定义行为,结果不可预期。
对于函数指针有两个用途和操作:
- 调用函数 (最重要)
- 做函数的参数
- 练习一:
(*(void (*)())0)();
【解释】:将整数 0 强制类型转换为一个参数为空、返回值是 void 的函数指针,再进行解引用得到函数,在与()结合完成函数调用
- 入手点是最后一个
()
,它应是进行函数的调用,那么前面的部分应该就是一个函数。 - 再进入圆括号,看到解引用符号
*
,与它同级的还有一个整数0
,在整型数据之前的操作可以猜测是一个强制类型转换的过程。 - 什么样的数据解引用会返回一个函数?函数指针。所以
void (*)()
就是我们要找的函数指针。
【运行情况】:内存访问出错,无法正常执行
将数字0
强制转换为指针,那么指针指向了内存地址为0
的内存,这个指针就是一个空指针NULL
,空指针是一个无效指针,再进行解引用就是一个未定义行为,如果运行环境是Linux
,就会产生段错误。
- 练习二:
void (*signal(int , void(*)(int)))(int);
【解释】:声明了一个 signal 函数,函数的参数是一个 int 类型变量 与 一个 void(*)(int) 类型的函数指针变量,函数返回值也是一个 void(*)(int) 类型的参数
- 先看到
void
关键字,代码最后是一个(int)
,那么它就不是一个函数的调用。有返回值类型、有参数类型,更像是一个函数的声明。 - 去掉
void
和(int)
进入圆括号,发现有一个解引用*
,那么最外层就是一个函数指针,类型是void (*)(int)
。 - 再观察
signal
函数,它之后包围的参数是int
和void(*)(int)
的两个类型,说明这是它的两个形参的类型,它是一个函数。
其实像如下这种简单的类型重命名转换一下,就会更直观一些:
typedef void(*T)(int); //创建了一个类型的别名 T
T signal(int,T); //原代码就可以等价转换成这样
函数指针数组
它是一个数组,把函数的地址存到一个数组中,那这个数组就叫函数指针数组。
函数指针数组的定义:
int (*arr[10]])();
arr
先和 []
结合,说明arr
是数组,数组的内容int (*)()
类型的函数指针。
函数指针数组最重要的一个用途:转移表,也称表驱动,它可以大大降低圈复杂度(https://baike.baidu.com/item/圈复杂度/828737?fr=aladdin),可以简化代码,让代码效率更高。
请看代码:
int add(int a, int b){ //加法函数
return a + b;
}
int sub(int a, int b){ //减法函数
return a - b;
}
int mul(int a, int b){ //乘法函数
return a * b;
}
int div(int a, int b){ //除法函数
return a / b;
}
//主函数开始
int main(){
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input){
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1)){
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0;
}
功能演示:
这样在代码中就可以直接使用定义函数指针数组的表驱动法,避免编写多个if…else…
语句这样高圈复杂度的嵌套方式,简化代码。
到此,我们发现还有 指向函数指针数组的指针、函数指针数组指针数组……可以无线叠加下去,原理相同,根据实际情况使用。
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数由该函数的实现方非直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
【简单地说就是:把函数中灵活的逻辑单独提取出来,然后在不同的场景下进行自定制,使函数功能更加强大】
下面使用一个简单的升序冒泡排序
进行演示:
void BubbleSort(int *str,int size) {
for (int bound = 0; bound < size; bound++) {
for (int cur = size - 1; cur > bound; cur--) {
if (str[cur - 1] < str[cur]) {
int temp = str[cur];
str[cur] = str[cur - 1];
str[cur - 1] = temp;
}
}
}
}
int main() {
int arr[10] = { 1,6,5,4,3,7,8,9,2,0 };
int size = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, size);
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
system("pause");
return 0;
}
其实代码第4行if (str[cur - 1] < str[cur])
这个条件就在描述排序规则,当把这个小于号<
改成大于号>
,整个函数就成为了一个降序的冒泡排序了。
那么如何实现一个函数,既可实现升序,又可实现降序,甚至希望通过绝对值大小排序,这时回调函数就可以发挥功效了:
typedef int(*T)(int, int);
//参数中引入了一个函数指针,这个函数指针描述了排序规则
void BubbleSort(int *str, int size,T cmp) {
for (int bound = 0; bound < size; bound++) {
for (int cur = size - 1; cur > bound; cur--) {
if (cmp(str[cur - 1],str[cur]) == 0) { //改动点1
int temp = str[cur];
str[cur] = str[cur - 1];
str[cur - 1] = temp;
}
}
}
}
int Less(int x, int y) { //加入升序排序规则函数
return x < y ? 1 : 0;
}
int Greater(int x, int y) { //加入降序排序规则函数
return x > y ? 1 : 0;
}
int main() {
int arr[10] = { 1,6,5,4,3,7,8,9,2,0 };
int size = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, size, Less); //升序排序 改动点2
BubbleSort(arr, size, Greater); //降序排序
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
system("pause");
return 0;
}
这里的Less
函数就称为回调函数。
现在的这个冒泡排序就大有可为了,他可以根据你任意给定的排序方式进行排序,非常灵活。
假设如果想实现绝对值排序,只需要编写一个绝对值排序的规则函数作为函数指针传入函数就可以了:
int Abs(int x, int y) {
return abs(x) < abs(y) ? 1 : 0; //使用abs函数要包含头文件 math.h
}
直接在主函数中调用即可:
BubbleSort(arr, size, Abs);
【注】:回调函数调用时机不是由调用者来决定,而是由操作系统或代码框架决定。后世的编程语言中引入了一个比回调更有意义的概念:闭包,之后遇到相关场景再进行讲解。
- 小结
指针相关的一些较难的常见用法至此就讲解完毕了,本篇博客旨在提高代码效率,如若使用一般逻辑可以完成,也大可不必使用这些内容,仅供推荐。希望读者们可以有所收获~