Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第九章 函数指针

在前面几章中,我们已经学习了函数的定义与调用,掌握了如何将一段逻辑封装起来并通过函数名反复使用。然而,函数名本身究竟是什么?它仅仅是一个符号吗?本章将回答这个问题,并由此引出一个重要的概念——函数指针。

函数指针允许我们将函数本身当作一种“数据“来存储和传递。掌握了函数指针,你就能写出更加灵活和通用的代码:同一个函数可以根据传入的不同函数指针,表现出完全不同的行为。这种技术被称为回调(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); 为例:

  1. 给函数名加上 *int *add(int a, int b);
  2. *add 看作一个整体,用括号括起来int (*add)(int a, int b);
  3. 去掉形参名,只保留类型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 函数本身完全不知道要对 xy 做什么操作——它只负责调用 operation 并打印结果。具体的行为——加还是乘——由 main 函数通过传入不同的函数指针来决定。这就是函数指针作为类型的核心用途:让函数的行为由调用者来定制

1. 回调函数

上面这种“把函数指针传给另一个函数,让它在合适的时机反过来调用“的用法,有一个专门的名字——回调函数(Callback)。被传入的那个函数(比如 addmultiply)叫做回调函数;接收函数指针、在内部调用它的那个函数(比如 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_valuesquare_valueadd_ten 都满足这个签名,所以都可以赋给 funcfor_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;
}