C的笛卡尔之梦

 主页   资讯   文章   代码   电子书 

练习18:函数指针

原文:Exercise 18: Pointers To Functions

函数在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,而你可以将它用作函数。这类似于指向数组的指针可以表示所指向的数组。指向函数的指针也可以用作表示所指向的函数,只不过是不同的名字。

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添加到相同的指针语法之前,然后你就可以将那个名字用作类型了。我使用下面的代码来演示这一特性:

#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的地方,在后面我像intchar类型那样,在bubble_sorttest_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_orderreverse_orderstrange_order的名字作为函数来调用test_sorting。C编译器会找到这些函数的地址,并且生成指针用于test_sorting。如果你看一眼test_sorting你会发现它把这些函数传给了bubble_sort,并不关心它们是做了什么。只要符合compare_cb原型的东西都有效。

ex18.c:109

我们在最后释放了我们创建的整数数组。

你会看到什么

运行这个程序非常简单,但是你要尝试不同的数字组合,甚至要尝试输入非数字来看看它做了什么:

$ 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下面:

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使它接收任意的排序函数和排序函数的比较回调。并使用它来测试两种排序算法。