函数指针有什么用?
C 语言指针除了可以指向变量之外,还可以声明指向函数的指针,这种指针被称为函数指针。这个复杂的玩意儿到底有何用处?
- 函数指针可以作为另一个函数的参数,告诉该函数要使用哪一个函数。例如,回调函数就是用函数指针。
- 函数指针可以作为结构体的成员。在结构体中不能直接声明函数,但是可以声明函数指针。这样就可以用结构体替代 C++ 中的类,这也体现了面向对象是一种思想,而不是语言,因为使用 C 语言一样可以实现面向对象编程(面向对象编程这里不过多介绍)。
这里主要介绍函数指针的第一种用法 —— 作为函数的参数。用函数指针作为函数的参数有什么好处?
Q:可以抽象出更通用的函数,通过传递函数指针来实现不同的效果。比较经典的函数指针作为函数参数的例子 —— C 库的 qsort() 函数。
// <stdlib.h> 中 qsort() 函数原型:
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
排序数组涉及比较两个元素,以确定先后。如果元素是数字,可以使用 > 运算符;如果元素是字符串或结构,就要调用函数进行比较。C 库中的 qsort() 函数可以处理任意类型的数组,但是要告诉 qsort() 使用哪个函数来比较元素。为此, qsort() 函数的参数列表中,有一个参数接受指向函数的指针。然后,qsort() 函数使用该函数提供的方案进行排序,无论这个数组中的元素是整数、字符串还是结构。
下面演示一下使用 函数指针作为参数的 qsort() 函数的灵活性:
#include<stdio.h>
#include<stdlib.h>
int compare_int1(const void* a, const void* b)
{
int *c = (int*) a;
int *d = (int*) b;
return *c - *d;
}
int compare_int2(const void* a, const void* b)
{
int *c = (int*) a;
int *d = (int*) b;
return *d - *c;
}
void print_arr(int arr[], size_t num)
{
for(int i = 0; i < num; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main()
{
int a[10] = {35, 12, 55, 27, 34, 59, 94, 0, 25, 87};
printf("排序前 a 数组:");
print_arr(a, 10);
printf("-----------------------\n");
qsort(a, 10, sizeof(a[0]), compare_int1);
printf("----- 从小到大排序 -----\n");
printf("排序后 a 数组:");
print_arr(a, 10);
printf("-----------------------\n");
printf("----- 从大到小排序 -----\n");
printf("排序后 a 数组:");
qsort(a, 10, sizeof(a[0]), compare_int2);
print_arr(a, 10);
return 0;
}
// 结果:
排序前 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指针的函数
函数指针的赋值
声明了函数指针后,可以把类型匹配的函数地址赋给它。
- 直接用函数名为函数指针赋值;
- 用(&函数名)为函数指针赋值。
例如:
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 认为这两种形式等价。后续的标准也延续了这种矛盾的和谐。