# 练习18:函数指针
> 原文:[Exercise 18: Pointers To Functions](http://c.learncodethehardway.org/book/ex18.html)
> 译者:[飞龙](https://github.com/wizardforcel)
函数在C中实际上只是指向程序中某一个代码存在位置的指针。就像你创建过的结构体指针、字符串和数组那样,你也可以创建指向函数的指针。函数指针的主要用途是向其他函数传递“回调”,或者模拟类和对象。在这个练习中我们会创建一些回调,并且下一节我们会制作一个简单的对象系统。
函数指针的格式类似这样:
```c
int (*POINTER_NAME)(int a, int b)
```
记住如何编写它的一个方法是:
+ 编写一个普通的函数声明:`int callme(int a, int b)`
+ 将函数用指针语法包装:`int (*callme)(int a, int b)`
+ 将名称改成指针名称:`int (*compare_cb)(int a, int b)`
这个方法的关键是,当你完成这些之后,指针的变量名称为`compare_cb`,而你可以将它用作函数。这类似于指向数组的指针可以表示所指向的数组。指向函数的指针也可以用作表示所指向的函数,只不过是不同的名字。
```c
int (*tester)(int a, int b) = sorted_order;
printf("TEST: %d is same as %d\n", tester(2, 3), sorted_order(2, 3));
```
即使是对于返回指针的函数指针,上述方法依然有效:
+ 编写:`char *make_coolness(int awesome_levels)`
+ 包装:`char *(*make_coolness)(int awesome_levels)`
+ 重命名:`char *(*coolness_cb)(int awesome_levels)`
需要解决的下一个问题是使用函数指针向其它函数提供参数比较困难,比如当你打算向其它函数传递回调函数的时候。解决方法是使用`typedef`,它是C的一个关键字,可以给其它更复杂的类型起个新的名字。你需要记住的事情是,将`typedef`添加到相同的指针语法之前,然后你就可以将那个名字用作类型了。我使用下面的代码来演示这一特性:
```c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
/** Our old friend die from ex17. */
void die(const char *message)
{
if(errno) {
perror(message);
} else {
printf("ERROR: %s\n", message);
}
exit(1);
}
// a typedef creates a fake type, in this
// case for a function pointer
typedef int (*compare_cb)(int a, int b);
/**
* A classic bubble sort function that uses the
* compare_cb to do the sorting.
*/
int *bubble_sort(int *numbers, int count, compare_cb cmp)
{
int temp = 0;
int i = 0;
int j = 0;
int *target = malloc(count * sizeof(int));
if(!target) die("Memory error.");
memcpy(target, numbers, count * sizeof(int));
for(i = 0; i < count; i++) {
for(j = 0; j < count - 1; j++) {
if(cmp(target[j], target[j+1]) > 0) {
temp = target[j+1];
target[j+1] = target[j];
target[j] = temp;
}
}
}
return target;
}
int sorted_order(int a, int b)
{
return a - b;
}
int reverse_order(int a, int b)
{
return b - a;
}
int strange_order(int a, int b)
{
if(a == 0 || b == 0) {
return 0;
} else {
return a % b;
}
}
/**
* Used to test that we are sorting things correctly
* by doing the sort and printing it out.
*/
void test_sorting(int *numbers, int count, compare_cb cmp)
{
int i = 0;
int *sorted = bubble_sort(numbers, count, cmp);
if(!sorted) die("Failed to sort as requested.");
for(i = 0; i < count; i++) {
printf("%d ", sorted[i]);
}
printf("\n");
free(sorted);
}
int main(int argc, char *argv[])
{
if(argc < 2) die("USAGE: ex18 4 3 1 5 6");
int count = argc - 1;
int i = 0;
char **inputs = argv + 1;
int *numbers = malloc(count * sizeof(int));
if(!numbers) die("Memory error.");
for(i = 0; i < count; i++) {
numbers[i] = atoi(inputs[i]);
}
test_sorting(numbers, count, sorted_order);
test_sorting(numbers, count, reverse_order);
test_sorting(numbers, count, strange_order);
free(numbers);
return 0;
}
```
在这段程序中,你将创建动态排序的算法,它会使用比较回调对整数数组排序。下面是这个程序的分解,你应该能够清晰地理解它。
ex18.c:1~6
通常的包含,用于所调用的所有函数。
ex18.c:7~17
这就是之前练习的`die`函数,我将它用于错误检查。
ex18.c:21
这是使用`typedef`的地方,在后面我像`int`或`char`类型那样,在`bubble_sort`和`test_sorting`中使用了`compare_cb`。
ex18.c:27~49
一个冒泡排序的实现,它是整数排序的一种不高效的方法。这个函数包含了:
ex18.c:27
这里是将`typedef`用于` compare_cb`作为`cmp`最后一个参数的地方。现在它是一个会返回两个整数比较结果用于排序的函数。
ex18.c:29~34
栈上变量的通常创建语句,前面是使用`malloc`创建的堆上整数数组。确保你理解了`count * sizeof(int)`做了什么。
ex18.c:38
冒泡排序的外循环。
ex18.c:39
冒泡排序的内循环。
ex18.c:40
现在我调用了`cmp`回调,就像一个普通函数那样,但是不通过预先定义好的函数名,而是一个指向它的指针。调用者可以像它传递任何参数,只要这些参数符合`compare_cb` `typedef`的签名。
ex18.c:41-43
冒泡排序所需的实际交换操作。
ex18.c:48
最后返回新创建和排序过的结果数据`target`。
ex18.c:51-68
`compare_cb`函数类型三个不同版本,它们需要和我们所创建的`typedef`具有相同的定义。否则C编辑器会报错说类型不匹配。
ex18.c:74-87
这是`bubble_sort`函数的测试。你可以看到我同时将`compare_cb`传给了`bubble_sort`来演示它是如何像其它指针一样传递的。
ex18.c:90-103
一个简单的主函数,基于你通过命令行传递进来的整数,创建了一个数组。然后调用了`test_sorting`函数。
ex18.c:105-107
最后,你会看到`compare_cb`函数指针的`typedef`是如何使用的。我仅仅传递了`sorted_order`、`reverse_order`和`strange_order`的名字作为函数来调用`test_sorting`。C编译器会找到这些函数的地址,并且生成指针用于`test_sorting`。如果你看一眼`test_sorting`你会发现它把这些函数传给了`bubble_sort`,并不关心它们是做了什么。只要符合`compare_cb`原型的东西都有效。
ex18.c:109
我们在最后释放了我们创建的整数数组。
## 你会看到什么
运行这个程序非常简单,但是你要尝试不同的数字组合,甚至要尝试输入非数字来看看它做了什么:
```sh
$ make ex18
cc -Wall -g ex18.c -o ex18
$ ./ex18 4 1 7 3 2 0 8
0 1 2 3 4 7 8
8 7 4 3 2 1 0
3 4 2 7 1 0 8
$
```
## 如何使它崩溃
我打算让你做一些奇怪的事情来使它崩溃,这些函数指针都是类似于其它指针的指针,他们都指向内存的一块区域。C中可以将一种指针的指针转换为另一种,以便以不同方式处理数据。这些通常是不必要的,但是为了想你展示如何侵入你的电脑,我希望你把这段代码添加在`test_sorting`下面:
```c
unsigned char *data = (unsigned char *)cmp;
for(i = 0; i < 25; i++) {
printf("%02x:", data[i]);
}
printf("\n");
```
这个循环将你的函数转换成字符串,并且打印出来它的内容。这并不会中断你的程序,除非CPU和OS在执行过程中遇到了问题。在它打印排序过的数组之后,你所看到的是一个十六进制数字的字符串:
```
55:48:89:e5:89:7d:fc:89:75:f8:8b:55:fc:8b:45:f8:29:d0:c9:c3:55:48:89:e5:89:
```
这就应该是函数的原始的汇编字节码了,你应该能看到它们有相同的起始和不同的结尾。也有可能这个循环并没有获得函数的全部,或者获得了过多的代码而跑到程序的另外一片空间。这些不通过更多分析是不可能知道的。
## 附加题
+ 用十六进制编辑器打开`ex18`,接着找到函数起始处的十六进制代码序列,看看是否能在原始程序中找到函数。
+ 在你的十六进制编辑器中找到更多随机出现的东西并修改它们。重新运行你的程序看看发生了什么。字符串是你最容易修改的东西。
+ 将错误的函数传给`compare_cb`,并看看C编辑器会报告什么错误。
+ 将`NULL`传给它,看看程序中会发生什么。然后运行`Valgrind`来看看它会报告什么。
+ 编写另一个排序算法,修改`test_sorting`使它接收任意的排序函数和排序函数的比较回调。并使用它来测试两种排序算法。
- 笨办法学C 中文版
- 前言
- 导言:C的笛卡尔之梦
- 练习0:准备
- 练习1:启用编译器
- 练习2:用Make来代替Python
- 练习3:格式化输出
- 练习4:Valgrind 介绍
- 练习5:一个C程序的结构
- 练习6:变量类型
- 练习7:更多变量和一些算术
- 练习8:大小和数组
- 练习9:数组和字符串
- 练习10:字符串数组和循环
- 练习11:While循环和布尔表达式
- 练习12:If,Else If,Else
- 练习13:Switch语句
- 练习14:编写并使用函数
- 练习15:指针,可怕的指针
- 练习16:结构体和指向它们的指针
- 练习17:堆和栈的内存分配
- 练习18:函数指针
- 练习19:一个简单的对象系统
- 练习20:Zed的强大的调试宏
- 练习21:高级数据类型和控制结构
- 练习22:栈、作用域和全局
- 练习23:认识达夫设备
- 练习24:输入输出和文件
- 练习25:变参函数
- 练习26:编写第一个真正的程序
- 练习27:创造性和防御性编程
- 练习28:Makefile 进阶
- 练习29:库和链接
- 练习30:自动化测试
- 练习31:代码调试
- 练习32:双向链表
- 练习33:链表算法
- 练习34:动态数组
- 练习35:排序和搜索
- 练习36:更安全的字符串
- 练习37:哈希表
- 练习38:哈希算法
- 练习39:字符串算法
- 练习40:二叉搜索树
- 练习41:将 Cachegrind 和 Callgrind 用于性能调优
- 练习42:栈和队列
- 练习43:一个简单的统计引擎
- 练习44:环形缓冲区
- 练习45:一个简单的TCP/IP客户端
- 练习46:三叉搜索树
- 练习47:一个快速的URL路由
- 后记:“解构 K&R C” 已死
- 捐赠名单