函数指针有什么用?

C 语言指针除了可以指向变量之外,还可以声明指向函数的指针,这种指针被称为函数指针。这个复杂的玩意儿到底有何用处

  1. 函数指针可以作为另一个函数的参数,告诉该函数要使用哪一个函数。例如,回调函数就是用函数指针。
  2. 函数指针可以作为结构体的成员。在结构体中不能直接声明函数,但是可以声明函数指针。这样就可以用结构体替代 C++ 中的类,这也体现了面向对象是一种思想,而不是语言,因为使用 C 语言一样可以实现面向对象编程(面向对象编程这里不过多介绍)。

这里主要介绍函数指针的第一种用法 —— 作为函数的参数。用函数指针作为函数的参数有什么好处
Q:可以抽象出更通用的函数,通过传递函数指针来实现不同的效果。比较经典的函数指针作为函数参数的例子 —— C 库的 qsort() 函数。

  1. // <stdlib.h> 中 qsort() 函数原型:
  2. void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))

排序数组涉及比较两个元素,以确定先后。如果元素是数字,可以使用 > 运算符;如果元素是字符串或结构,就要调用函数进行比较。C 库中的 qsort() 函数可以处理任意类型的数组,但是要告诉 qsort() 使用哪个函数来比较元素。为此, qsort() 函数的参数列表中,有一个参数接受指向函数的指针。然后,qsort() 函数使用该函数提供的方案进行排序,无论这个数组中的元素是整数、字符串还是结构。
下面演示一下使用 函数指针作为参数的 qsort() 函数的灵活性:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int compare_int1(const void* a, const void* b)
  4. {
  5. int *c = (int*) a;
  6. int *d = (int*) b;
  7. return *c - *d;
  8. }
  9. int compare_int2(const void* a, const void* b)
  10. {
  11. int *c = (int*) a;
  12. int *d = (int*) b;
  13. return *d - *c;
  14. }
  15. void print_arr(int arr[], size_t num)
  16. {
  17. for(int i = 0; i < num; i++)
  18. printf("%d ", arr[i]);
  19. printf("\n");
  20. }
  21. int main()
  22. {
  23. int a[10] = {35, 12, 55, 27, 34, 59, 94, 0, 25, 87};
  24. printf("排序前 a 数组:");
  25. print_arr(a, 10);
  26. printf("-----------------------\n");
  27. qsort(a, 10, sizeof(a[0]), compare_int1);
  28. printf("----- 从小到大排序 -----\n");
  29. printf("排序后 a 数组:");
  30. print_arr(a, 10);
  31. printf("-----------------------\n");
  32. printf("----- 从大到小排序 -----\n");
  33. printf("排序后 a 数组:");
  34. qsort(a, 10, sizeof(a[0]), compare_int2);
  35. print_arr(a, 10);
  36. return 0;
  37. }
// 结果:
排序前 a 数组:35 12 55 27 34 59 94 0 25 87 
-----------------------
----- 从小到大排序 -----
排序后 a 数组:0 12 25 27 34 35 55 59 87 94
-----------------------
----- 从大到小排序 -----
排序后 a 数组:94 87 59 55 35 34 27 25 12 0

可以看到,通过传递不同的函数可以让 qsort() 函数分别实现从小到大排序和从大到小排序的效果,这样不需要我们分别创建两个排序函数,大大减少代码量。除了基本数据类型的排序,也可以通过传递函数指针的方式让 sqort() 函数对字符串数组、结构体数组进行排序,而且我们可以自定义按照什么规则进行排序。

PS:学过 Java 的朋友应该会比较熟悉,在 Java 中是用过 Comparator 接口来传递比较规则的,Comparator 接口中只有一个 compare() 函数,实际上也就是传递了一个函数指针。

如何定义函数指针?

前面我们已经了解过了使用函数指针的优点,下面我们就开始学习函数指针的知识吧。

回顾下我们之前学习的指针。假设有一个指向 int 类型变量的指针,该指针储存着这个 int 类型变量储存在内存位置的地址。同样,函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址。
声明一个数据指针时,必须声明指针所指向的数据类型。同样的,声明一个函数指针时,必须声明指针指向的函数类型,即函数的返回类型和形参类型
例如,考虑下面的函数原型:

void ToUpper(char *); // 把字符串中的字符转换成大写字符

ToUpper() 函数的类型是“带 char * 类型参数、返回类型是 void 的函数”。下面声明了一个指针 pf 指向该函数类型:

void (*pf)(char *); // pf 是一个指向函数的指针

从该声明可以看出,第1对圆括号把 和 pf 括起来,表明 pf 是一个指向函数的指针。因此,(pf) 是一个参数列表为 char 、返回类型为 void 的函数。
注意:把函数名 ToUpper 替换为表达式 (
pf) 是创建指向函数指针最简单的方式。所以,如果想声明一个指向某类型函数的指针,可以写出该函数的原型后把函数名替换成 (pf) 形式的表达式,创建函数指针声明。前面提到过,由于运算符优先级的规则,在声明函数指针时必须把 和指针名括起来。如果省略第1个圆括号会导致完全不同的情况:

void *pf(char *); // pf 是一个返回void指针的函数

函数指针的赋值

声明了函数指针后,可以把类型匹配的函数地址赋给它。

  1. 直接用函数名为函数指针赋值;
  2. 用(&函数名)为函数指针赋值。

例如:

void ToUpper(char *); // 函数
int round(double); // 函数

void (pf)(char*); // 函数指针
int main(void) {
    pf = ToUpper; // 正确
    pf = &ToUpper; // 正确
    pf = ToUpper(); // 错误,这是调用函数。
    pf = round; // 错误,round和指针类型不匹配。
}

函数指针的使用

给函数指针赋值时有两种写法,同样,使用函数组指针也同样有两种对应的写法。

void ToUpper(char *); // 函数
void (pf)(char*); // 函数指针
int main(void) {
    pf = ToUpper;
    char str[] = "hello";
    pf(str); // 正确
    (*pf)(str); // 正确
}

之所以有两种写法是由于历史的原因,贝尔实验室的 C 和 UNIX 的开发者采用第1种形式,而伯克利的 UNIX 推广者却采用第2种形式。K&R C 不允许第2种形式。但是,为了与现有代码兼容,ANSI C 认为这两种形式等价。后续的标准也延续了这种矛盾的和谐。