前面咱们把指针和数组、指针和字符串都聊完了。你发现没有,指针这东西,指来指去,指的都是数据——数组里的元素、字符串里的字符,全是内存里存的那些0和1。
但今天我要告诉你一个更神奇的事儿:指针不光能指向数据,还能指向函数。
啥意思?函数不是代码吗?代码也能被指针指?
没错。在C语言里,函数在编译之后也是一段机器指令,存在内存里。既然存在内存里,它就有地址。既然有地址,那指针就能存它。
这就是函数指针——一个专门用来存函数地址的指针。
先回忆一下普通指针长啥样
int a = 10; int *p = &a; // p是指向int变量的指针
p存的是a的地址,通过*p可以访问a的值。
那函数指针呢?它存的是函数的地址,通过它可以调用那个函数。
函数指针怎么定义?
先看格式,别被吓到:
返回值类型 (*指针变量名)(参数类型列表);
举个例子。假设你有一个函数:
int add(int a, int b) { return a + b; }
这个函数接受两个int,返回一个int。那指向它的函数指针就这么定义:
int (*p)(int, int);
拆开看:
-
int是函数的返回值类型 -
(*p)是指针变量,名字叫p,注意括号不能省 -
(int, int)是这个函数接受的参数类型
然后把函数的地址赋给它:
p = add; // 或者 p = &add;
注意:函数名本身就代表函数的地址,所以add和&add是一样的。跟数组名一个道理——数组名是首元素地址,函数名是函数的地址。
赋值之后,你就可以通过p来调用这个函数了:
int result = p(3, 5); // 等价于调用 add(3, 5)
你甚至可以直接用(*p)(3, 5),效果一样。但一般咱们写p(3, 5)更简洁。
完整例子:
#include <stdio.h> int add(int a, int b) { return a + b; } int main() { int (*p)(int, int); // 定义一个函数指针 p = add; // 让它指向add函数 int result = p(3, 5); // 通过指针调用函数 printf("%d\n", result); // 输出8 return 0; }
你看,p就是个指针,只不过它指的不是数据,是函数。你调用p(3,5),实际上就是调用了add(3,5)。
为啥要搞这么个东西?直接用函数名不行吗?
行,当然行。但函数指针有一个特别大的好处:你可以把函数当参数传给另一个函数。
这就是回调函数的思维。我给你举个例子你就明白了。
假设你要写一个排序函数,能对数组排序。但是排序的时候,到底是从小到大排还是从大到小排?你希望调用者来定,而不是写死在排序函数里。
怎么办?把“怎么比较”这件事做成一个函数,然后把函数的指针传进来。
#include <stdio.h> // 从小到大比较 int ascending(int a, int b) { return a > b; // a大于b时返回真,表示要交换 } // 从大到小比较 int descending(int a, int b) { return a < b; // a小于b时返回真,表示要交换 } // 冒泡排序,第三个参数是一个函数指针 void bubble_sort(int arr[], int n, int (*compare)(int, int)) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (compare(arr[j], arr[j + 1])) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } int main() { int a[6] = {5, 2, 8, 1, 9, 3}; bubble_sort(a, 6, ascending); for (int i = 0; i < 6; i++) printf("%d ", a[i]); printf("\n"); // 输出 1 2 3 5 8 9 bubble_sort(a, 6, descending); for (int i = 0; i < 6; i++) printf("%d ", a[i]); printf("\n"); // 输出 9 8 5 3 2 1 return 0; }
你看,bubble_sort本身不关心你怎么比,它只知道“你来告诉我怎么比”。你传ascending进去,它就从小到大排;传descending进去,就从大到小排。排序逻辑写一遍,比较逻辑可以随便换。
这就是函数指针最大的威力——把函数当数据一样传来传去,让代码的灵活性直接上一个台阶。
函数指针的几种写法
咱们把常见的函数指针写法捋一遍:
// 1. 指向无参数、无返回值的函数 void (*p1)(); // 2. 指向有一个int参数、返回int的函数 int (*p2)(int); // 3. 指向有两个int参数、返回int的函数 int (*p3)(int, int); // 4. 指向有两个int参数、返回float的函数 float (*p4)(int, int); // 5. 指向有两个int参数、返回指向int的指针的函数 int* (*p5)(int, int);
看着复杂,其实就是把函数的“签名”抄下来,把函数名换成(*指针名),参数只留类型,就这么简单。
函数指针的经典应用:菜单系统
函数指针在实际开发里用得特别多,最典型的就是菜单系统。你写一个程序,有多个功能选项,用户输入数字选择功能。用函数指针可以把“功能列表”做成一个数组。
#include <stdio.h> void func1() { printf("执行功能1\n"); } void func2() { printf("执行功能2\n"); } void func3() { printf("执行功能3\n"); } int main() { // 函数指针数组 void (*menu[3])() = {func1, func2, func3}; int choice; printf("请输入1-3选择功能:"); scanf("%d", &choice); if (choice >= 1 && choice <= 3) { menu[choice - 1](); // 调用对应的函数 } else { printf("无效选择\n"); } return 0; }
这个例子特别有代表性。你用menu数组存了三个函数的地址,用户选1就调menu[0](),选2就调menu[1]()。以后想增加功能,只需要在数组里加一个函数指针就行了,主逻辑不用动。
函数指针做参数——回调函数的本质
咱们再来看一个例子,这次是把函数指针当参数传,实现一个“计算器”:
#include <stdio.h> 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 calculate(int a, int b, int (*op)(int, int)) { return op(a, b); } int main() { printf("%d\n", calculate(10, 5, add)); // 15 printf("%d\n", calculate(10, 5, sub)); // 5 printf("%d\n", calculate(10, 5, mul)); // 50 return 0; }
calculate这个函数本身不关心是加是减还是乘,它只负责“把a和b交给op指向的函数去算”。你传不同的函数进去,它就算出不同的结果。这就是回调函数——你调用calculate,calculate回头调用你传进去的那个函数。
几个常见的坑
坑1:忘了写括号
int *p(int, int); // 错误理解:这是个函数声明,不是函数指针 int (*p)(int, int); // 正确:这才是函数指针
int *p(int, int)会被编译器理解成“p是一个函数,接收两个int,返回int*”。因为()的优先级比*高,p(int,int)先结合,说明p是个函数,前面的*说明它返回指针。
int (*p)(int, int)因为加了括号,*先跟p结合,说明p是个指针,后面的(int, int)说明它指向的是一个函数。这就是括号的重要性。
坑2:函数指针的类型必须匹配
int add(int a, int b) { return a + b; } int main() { void (*p)(); // 指向无参无返回值的函数 p = add; // 类型不匹配!但有些编译器只给警告 p(); // 调用方式不对,参数传不进去 return 0; }
函数指针的类型必须跟它指向的函数完全一致——返回值类型、参数个数、参数类型,一个都不能差。对不上虽然有时候能编译,但运行起来行为是不可预测的。
坑3:函数指针没初始化就调用
int (*p)(int, int); p(3, 5); // 危险!p指向未知地址,程序崩溃
跟普通指针一样,用之前一定要让它指向一个合法的函数。
坑4:把函数指针跟返回指针的函数搞混
int* func(int, int); // func是一个函数,返回int* int (*p)(int, int); // p是一个指针,指向返回int的函数
这俩长得像,但意思完全不同。一个是函数,一个是指针。分清楚的关键是看*跟谁挨着。
总结一下
函数指针就一句话:它是一个指针,存的是函数的地址,通过它你可以调用那个函数。
定义格式:返回值类型 (*指针名)(参数类型列表);
核心用法:
-
把函数当参数传给另一个函数(回调)
-
把函数存进数组,做成“功能表”
-
在运行时动态决定调用哪个函数
用的时候记住两条:
-
函数名本身就是地址,赋值的时候直接写函数名就行,不用加
&。 -
通过函数指针调用函数,写
p(参数)或者(*p)(参数)都行,前者更简洁。
函数指针是C语言里“把函数当数据用”的核心工具。你现在可能觉得有点绕,但多写几个例子就顺了。