Menu

C 语言函数指针:语法、回调函数和 qsort

函数也有地址,函数指针就是用来存放地址的。本页拆解函数指针的声明语法,展示 typedef 如何让它变得易读,并逐步讲到回调函数、带自定义比较函数的 qsort 和分派表。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

编译后的代码和数据一样存放在内存中,所以函数也有地址。函数指针就是存放这种地址的变量。一旦能把函数存起来,你就可以把它作为参数传递,把一组函数放进表里,或者在运行时从中选择。

自定义排序规则、事件处理函数、插件接口和状态机,背后都是这种能力。它也是 C 语言中最容易让读者停下来眯眼细看的语法,所以我们就从语法讲起。

声明语法

先看一个普通函数:

int add(int a, int b);

要声明一个能存放它地址的指针,把名字换成 (*name):

int (*op)(int, int);

从里往外读:op 是一个指针,指向一个函数,该函数接收 (int, int),返回 int。

括号不是装饰。没有它们,* 就会和返回类型结合:

int (*f)(int);   // pointer to a function taking int, returning int
int *g(int);     // FUNCTION taking int, returning int* - completely different

下面是一个完整可运行的例子:

两种调用写法都可以,因为函数标识符会自动转换为指针。写 op(10, 3) 就好,(*op)(10, 3) 是旧时代遗留下来的写法。

关于最后那个 printf 说明一点:%p 期望的是对象指针,而函数指针在标准中属于另一类,所以打印它严格来说是不可移植的。那个强制转换能让编译器在支持这种做法的平台上保持安静;你很少需要打印函数指针。

用 typedef 让它易读

声明语法很快就会变得难看,而 typedef 是标准的解决办法:

对比一下 int apply(BinaryOp op, int x, int y) 和原始写法 int apply(int (*op)(int, int), int x, int y)。含义相同,但其中一个一眼就能看懂。typedef 的一般规则请参见 typedef。

回调函数:让调用者提供行为

回调函数是你交给另一个函数的函数,让它在合适的时机回过头来调用你。这样,一段代码负责完成工作的整体结构,而具体的判断由调用者提供。

没有函数指针,你就得写 count_even、count_positive 和 count_big,同一个循环复制三份。有了函数指针,循环只写一次,而判断条件成了参数。

真正的回报:qsort

标准库的 qsort 是你真正会用到的函数指针例子。它能对任何类型的数组排序,而做到这一点的方式是:除了元素的大小和你提供的比较函数之外,它对元素一无所知。

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

比较函数接收两个 const void *,即两个元素的地址;如果第一个应排在第二个前面,就返回负数,两者相等返回零,应排在后面则返回正数。

从这个比较函数中要养成两个习惯。永远不要写 return x - y;:它看起来很巧妙,但遇到很大的值或负值时会溢出,得到错误的符号,数组就排错了。先转换类型,再解引用,而不是反过来:*(const int *)a,因为对 void * 做 *a 没有东西可读。

同样的机制可以按任意字段对结构体排序:

(q->score > p->score) - (q->score < p->score) 这个惯用写法恰好返回 -1、0 或 1,不用减法,也不会溢出。而 strcmp 本身就符合这种返回值约定,所以字符串比较函数只需一行。

函数指针表

函数指针数组能把一连串 if/else 变成一次查表。解释器就是这样分派操作码的,菜单系统也是这样执行命令的。

现在新增一种运算只需加一行,而不用修改 switch。再结合 结构体,你就得到了 C 语言中标准的插件结构:在这门语言里,一个由函数指针组成的结构体就是“接口”的含义。

容易踩的坑

签名必须完全匹配。 函数指针的类型包括参数类型和返回类型。把签名不匹配的函数赋给它违反了约束,而通过类型错误的指针调用函数,即使“能用”也是未定义行为。不要为了消除警告去强制转换比较函数的签名,应该修好比较函数。

空的函数指针仍然是空指针。 初始化为 NULL,调用前先检查,和数据指针完全一样:

if (handler != NULL) {
    handler(event);
}

关于这个检查为什么值得做,参见 空指针。

void 指针会失去类型安全,这时你自己就是类型系统。 在 qsort 比较函数内部,没有什么能阻止你转换成错误的类型。如果你用 compare_ints 去排序一个 double 数组,它能干净地通过编译,却得出毫无意义的结果。

声明符语法会嵌套。 一个返回函数指针的函数要写成 int (*get_op(char c))(int, int);。当你发现自己在写这种东西时,就该用 typedef 了:

typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c);           // the same thing, readable

常见问题

C 语言中怎么声明函数指针?

写出函数的签名,在函数名的位置写 (*name):int (*op)(int, int); 把 op 声明为一个指针,指向接收两个 int、返回 int 的函数。*op 外面的括号是必需的:没有它们,你声明的是一个返回指针的函数。

怎么通过指针调用函数?

op(3, 4) 或 (*op)(3, 4) 都可以,两者都合法,效果也相同,因为函数标识符会自动转换为指针。现代 C 代码使用更简洁的 op(3, 4) 写法。

C 语言中什么是回调函数?

回调函数是你交给另一个函数的函数,让它可以回过头来调用你。qsort 是经典的例子:你传入一个比较函数,qsort 每当需要知道两个元素谁在前时就调用它。正因如此,同一个排序函数才能用于任何类型。

取函数地址时需要写 & 吗?

不需要。函数名本身就会转换为指向该函数的指针,所以 op = add; 和 op = &add; 是等价的。大多数 C 代码都省略 &。

Coddy 编程语言插图

和 Coddy 一起学编程

开始