第九章 函数指针
在前面几章中,我们已经学习了函数的定义与调用,掌握了如何将一段逻辑封装起来并通过函数名反复使用。然而,函数名本身究竟是什么?它仅仅是一个符号吗?本章将回答这个问题,并由此引出一个重要的概念——函数指针。
函数指针允许我们将函数本身当作一种“数据“来存储和传递。掌握了函数指针,你就能写出更加灵活和通用的代码:同一个函数可以根据传入的不同函数指针,表现出完全不同的行为。这种技术被称为回调(Callback),是 C 语言中实现可扩展程序设计的重要手段。
本章内容:
- 函数指针的声明、赋值与调用
- 回调函数的应用场景与设计模式
- 动手练习
一、函数指针基础
1. 函数名到底是什么?
在第一章,我们已经学会了如何定义和调用函数:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int result = add(3, 5); // 调用 add 函数
printf("3 + 5 = %d\n", result);
return 0;
}
这个写法你已经非常熟悉了:函数名(参数)。但请你停下来思考一个问题:add 是什么?它难道仅仅是一个名字吗?我们之前学过,变量名代表某块内存空间里的数据,而变量本身有地址,可以用 & 取出来。那么,函数能不能也被“定位“呢?答案是肯定的——让我们尝试把函数名保存到一个函数指针里,然后通过这个指针调用函数:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int (*p)(int, int) = add;
printf("可以把 add 保存到函数指针 p 中,并通过 p 调用:%d\n", p(3, 5));
return 0;
}
输出:
可以把 add 保存到函数指针 p 中,并通过 p 调用:8
你会发现,函数名也可以被当作“函数的位置“来使用。事实上,add 在很多表达式中会自动转换为指向这个函数的指针,就像数组名在很多表达式中会退化为数组首元素地址一样。在代码区中,编译器把 add 函数的机器指令放在某个位置,函数指针保存的就是这个函数入口位置。
既然函数名可以转换成函数指针,我们就可以把这个位置存到一个指针变量里,然后通过这个指针来调用函数——这就是函数指针。
2. 函数指针的声明
函数指针是一种特殊的指针,它指向的是函数,而不是普通数据。声明函数指针时,需要描述出这个函数的返回值类型和参数类型。其基本语法如下:
返回值类型 (*指针名)(参数类型列表);
让我们拆解这个语法:
返回值类型:所指函数的返回类型。(*指针名):星号表示这是一个指针,括号必须加,否则含义完全不同。(参数类型列表):所指函数的参数类型,只写类型,参数名可写可不写。
例如:
int (*p_func)(int, int); // p_func 是一个指针,指向"返回值为 int、接受两个 int 参数"的函数
读法是:p_func 是一个函数指针,指向的函数接收两个 int,返回 int。这行声明之后,p_func 就可以存放那些满足这个“签名“的函数的地址,比如前面的 add 函数(int add(int, int))就符合这个签名。
这里有一个关键的语法细节需要特别注意:声明函数指针时,(*指针名) 的括号绝对不能省略。试比较以下两种写法:
int (*p_func)(int, int); // 函数指针:p_func 是指针,指向"返回 int、接受两个 int"的函数
int *p_func(int, int); // 普通函数:p_func 是函数,返回 int *
在 C 语言里,() 的优先级比 * 高。如果不加括号,p_func 先与 (int, int) 结合,含义变成“一个名为 p_func 的函数,它返回 int *“。而加了括号 (*p_func),先让星号与名字结合,表示“这是一个指针”,然后再指向函数。这个语法稍显绕口,但你只需要记住一条规则:声明函数指针时,星号两边必须加括号。
💡 Tips:函数指针声明的推导思路
函数指针的语法初看有些绕,但你可以用以下三步从普通函数声明推导出来。以
int add(int a, int b);为例:
- 给函数名加上
*:int *add(int a, int b);- 把
*add看作一个整体,用括号括起来:int (*add)(int a, int b);- 去掉形参名,只保留类型:
int (*add)(int, int);这样就从普通函数声明得到了函数指针声明。第一步理解成“要声明一个指向函数的指针“,第二步用括号保证
*先与名字结合(否则()的高优先级会让add
3. 给函数指针赋值并调用
声明函数指针后,把函数名直接赋值给它即可:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int main(void)
{
// 声明函数指针,并让它指向 add
int (*p_func)(int, int);
p_func = add; // 直接用函数名赋值,不需要 &(写了 &add 也可以,效果一样)
// 通过函数指针调用 add
int result = p_func(10, 5); // 等价于 add(10, 5)
printf("10 + 5 = %d\n", result);
// 改为指向 subtract
p_func = subtract;
result = p_func(10, 5); // 等价于 subtract(10, 5)
printf("10 - 5 = %d\n", result);
return 0;
}
输出:
10 + 5 = 15
10 - 5 = 5
关于赋值和调用,有两点值得注意:
- 赋值:直接用函数名即可(
p_func = add),写成p_func = &add效果相同,因为函数名在表达式中会自动转换为函数指针 - 调用:写
p_func(参数)即可直接调用所指的函数,写成(*p_func)(参数)也是正确的,但p_func(参数)更为简洁
理解了函数指针的声明、赋值和调用之后,一个自然的问题是:函数指针的真正价值在哪里?
二、函数指针的应用
函数指针,归根结底也是一种类型。就像 int* 是指向 int 的指针类型、char* 是指向 char 的指针类型一样,int (*)(int, int) 就是指向“接收两个 int、返回 int“的函数的指针类型。第七章讲过,类型的用途就是用来声明变量、接收值、作为函数形参——函数指针类型也不例外。
你有没有想过这样一种场景:你想写一个函数 operate,让它对两个数做某种操作。但这个操作不由 operate 自己决定,而是由调用者来指定——传加法函数就做加法,传乘法函数就做乘法。换句话说,你需要在函数的形参里接收“另一个函数“。用什么类型来接收?
答案就是函数指针类型。
先看一个例子,直观感受一下:
#include <stdio.h>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// operate 的第三个参数是函数指针类型
// int (*operation)(int, int) 读作:
// operation 是一个指针,指向"接收两个 int、返回 int"的函数
void operate(int x, int y, int (*operation)(int, int))
{
int result = operation(x, y);
printf("结果:%d\n", result);
}
int main(void)
{
operate(10, 20, add); // 传入 add,结果 30
operate(10, 20, multiply); // 传入 multiply,结果 200
return 0;
}
输出:
结果:30
结果:200
分析这段代码:operate 函数本身完全不知道要对 x 和 y 做什么操作——它只负责调用 operation 并打印结果。具体的行为——加还是乘——由 main 函数通过传入不同的函数指针来决定。这就是函数指针作为类型的核心用途:让函数的行为由调用者来定制。
1. 回调函数
上面这种“把函数指针传给另一个函数,让它在合适的时机反过来调用“的用法,有一个专门的名字——回调函数(Callback)。被传入的那个函数(比如 add、multiply)叫做回调函数;接收函数指针、在内部调用它的那个函数(比如 operate)叫做调用方。
为什么需要回调函数?它解决的是一个非常实际的问题:分离“做什么“和“怎么做“。假设你要对一个数组中的每个元素做某种处理——翻倍、平方、加 10……如果没有回调函数,每种处理你都得写一个几乎一样的遍历函数,只有中间一行处理逻辑不同:
// 没有回调函数时:每种操作都要写一个独立的遍历函数(示意伪代码)
void double_all(int arr[], int size) { for (int i = 0; i < size; i++) arr[i] *= 2; }
void square_all(int arr[], int size) { for (int i = 0; i < size; i++) arr[i] = arr[i] * arr[i]; }
void add_ten_all(int arr[], int size) { for (int i = 0; i < size; i++) arr[i] += 10; }
// 每增加一种操作,就要复制粘贴一整个函数……
有了回调函数,遍历的逻辑只写一次,具体的操作由调用者通过函数指针传入:
#include <stdio.h>
// for_each 负责遍历数组,对每个元素调用 func
// func 的类型是 void (*)(int *) —— 指向"接收 int*、无返回值"的函数
void for_each(int arr[], int size, void (*func)(int *))
{
for (int i = 0; i < size; i++)
{
func(&arr[i]); // 回调:把元素的地址交给 func 处理
}
}
// 各种回调函数:签名必须是 void (int *)
void double_value(int *x) { *x = *x * 2; }
void square_value(int *x) { *x = (*x) * (*x); }
void add_ten(int *x) { *x = *x + 10; }
int main(void)
{
int numbers1[] = {1, 2, 3, 4, 5};
int numbers2[] = {1, 2, 3, 4, 5};
int numbers3[] = {1, 2, 3, 4, 5};
printf("原数组:");
for (int i = 0; i < 5; i++) printf("%d ", numbers1[i]);
printf("\n");
// 传入不同的回调函数,同一个 for_each 表现出完全不同的行为
for_each(numbers1, 5, double_value);
printf("翻倍后:");
for (int i = 0; i < 5; i++) printf("%d ", numbers1[i]);
printf("\n");
for_each(numbers2, 5, square_value);
printf("平方后:");
for (int i = 0; i < 5; i++) printf("%d ", numbers2[i]);
printf("\n");
for_each(numbers3, 5, add_ten);
printf("加10后:");
for (int i = 0; i < 5; i++) printf("%d ", numbers3[i]);
printf("\n");
return 0;
}
输出:
原数组:1 2 3 4 5
翻倍后:2 4 6 8 10
平方后:1 4 9 16 25
加10后:11 12 13 14 15
注意看 for_each 的第三个参数:void (*func)(int *)——这就是一个函数指针类型。它告诉编译器:func 是一个指针,指向的函数接收一个 int* 参数,没有返回值。调用者传入的 double_value、square_value、add_ten 都满足这个签名,所以都可以赋给 func。for_each 不需要知道传入的函数到底做了什么,它只负责在合适的时机调用 func(&arr[i])——这就是回调。
经典应用:自定义排序
C 标准库里的 qsort 是回调机制的经典范例。它可以对任意类型的数组排序,但怎么比较两个元素,由你通过函数指针告诉它:
#include <stdio.h>
#include <stdlib.h>
int compare_int(const void *a, const void *b)
{
int ia = *(const int *)a;
int ib = *(const int *)b;
if (ia < ib) return -1;
if (ia > ib) return 1;
return 0;
}
int main(void)
{
int arr[] = {42, 7, 99, 3, 15};
size_t size = sizeof arr / sizeof arr[0];
qsort(arr, size, sizeof(int), compare_int);
printf("排序后:");
for (size_t i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
输出:
排序后:3 7 15 42 99
qsort 的第四个参数类型是 int (*)(const void *, const void *)——一个函数指针类型。compare_int 就是我们传入的回调函数。qsort 在排序过程中,每次需要比较两个元素时,都会通过这个函数指针调用 compare_int。这种“框架提供机制,用户提供策略“的设计模式,正是回调函数最核心的价值所在。
以上我们学习了函数指针的声明与赋值、函数指针作为类型的核心用途、回调函数的设计模式。下面通过一个综合练习来巩固这些知识。
三、动手练习
#include <stdio.h>
// 回调函数类型:接收两个 int,返回 int
typedef int (*Operation)(int, int);
// 加法
int add(int a, int b) { return a + b; }
// 乘法
int multiply(int a, int b) { return a * b; }
// 取最大值
int max(int a, int b) { return a > b ? a : b; }
// 一个通用的"计算"函数,接收两个数和一种操作
int compute(int x, int y, Operation op)
{
return op(x, y);
}
int main(void)
{
int a = 12, b = 6;
printf("计算 a = %d, b = %d\n\n", a, b);
// 传入不同的回调函数
printf("加法结果:%d\n", compute(a, b, add));
printf("乘法结果:%d\n", compute(a, b, multiply));
printf("最大值结果:%d\n", compute(a, b, max));
return 0;
}