鍍金池/ 教程/ C/ 練習18:函數(shù)指針
練習9:數(shù)組和字符串
練習6:變量類型
練習3:格式化輸出
練習4:Valgrind 介紹
練習28:Makefile 進階
練習14:編寫并使用函數(shù)
練習21:高級數(shù)據(jù)類型和控制結(jié)構(gòu)
練習20:Zed的強大的調(diào)試宏
練習18:函數(shù)指針
練習0:準備
練習15:指針,可怕的指針
練習27:創(chuàng)造性和防御性編程
練習22:棧、作用域和全局
練習10:字符串數(shù)組和循環(huán)
練習8:大小和數(shù)組
練習16:結(jié)構(gòu)體和指向它們的指針
練習7:更多變量和一些算術(shù)
練習23:認識達夫設備
練習12:If,Else If,Else
練習2:用Make來代替Python
練習1:啟用編譯器
練習11:While循環(huán)和布爾表達式
練習5:一個C程序的結(jié)構(gòu)
練習24:輸入輸出和文件
練習25:變參函數(shù)
練習13:Switch語句
練習19:一個簡單的對象系統(tǒng)
練習26:編寫第一個真正的程序
導言:C的笛卡爾之夢
練習17:堆和棧的內(nèi)存分配

練習18:函數(shù)指針

函數(shù)在C中實際上只是指向程序中某一個代碼存在位置的指針。就像你創(chuàng)建過的結(jié)構(gòu)體指針、字符串和數(shù)組那樣,你也可以創(chuàng)建指向函數(shù)的指針。函數(shù)指針的主要用途是向其他函數(shù)傳遞“回調(diào)”,或者模擬類和對象。在這歌1練習中我們會創(chuàng)建一些回調(diào),并且下一節(jié)我們會制作一個簡單的對象系統(tǒng)。

函數(shù)指針的格式類似這樣:

int (*POINTER_NAME)(int a, int b)

記住如何編寫它的一個方法是:

  • 編寫一個普通的函數(shù)聲明:int callme(int a, int b)
  • 將函數(shù)用指針語法包裝:int (*callme)(int a, int b)
  • 將名稱改成指針名稱:int (*compare_cb)(int a, int b)

這個方法的關(guān)鍵是,當你完成這些之后,指針的變量名稱為compare_cb,而你可以將它用作函數(shù)。這類似于指向數(shù)組的指針可以表示所指向的數(shù)組。指向函數(shù)的指針也可以用作表示所指向的函數(shù),只不過是不同的名字。

int (*tester)(int a, int b) = sorted_order;
printf("TEST: %d is same as %d\n", tester(2, 3), sorted_order(2, 3));

即使是對于返回指針的函數(shù)指針,上述方法依然有效:

  • 編寫:char *make_coolness(int awesome_levels)
  • 包裝:char *(*make_coolness)(int awesome_levels)
  • 重命名:char *(*coolness_cb)(int awesome_levels)

需要解決的下一個問題是使用函數(shù)指針向其它函數(shù)提供參數(shù)比較困難,比如當你打算向其它函數(shù)傳遞回調(diào)函數(shù)的時候。解決方法是使用typedef,它是C的一個關(guān)鍵字,可以給其它更復雜的類型起個新的名字。你需要記住的事情是,將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;
}

在這段程序中,你將創(chuàng)建動態(tài)排序的算法,它會使用比較回調(diào)對整數(shù)數(shù)組排序。下面是這個程序的分解,你應該能夠清晰地理解它。

ex18.c:1~6

通常的包含,用于所調(diào)用的所有函數(shù)。

ex18.c:7~17

這就是之前練習的die函數(shù),我將它用于錯誤檢查。

ex18.c:21

這是使用typedef的地方,在后面我像intchar類型那樣,在bubble_sorttest_sorting中使用了compare_cb

ex18.c:27~49

一個冒泡排序的實現(xiàn),它是整數(shù)排序的一種不高效的方法。這個函數(shù)包含了:

ex18.c:27

這里是將typedef用于compare_cb作為cmp最后一個參數(shù)的地方?,F(xiàn)在它是一個會返回兩個整數(shù)比較結(jié)果用于排序的函數(shù)。

ex18.c:29~34

棧上變量的通常創(chuàng)建語句,前面是使用malloc創(chuàng)建的堆上整數(shù)數(shù)組。確保你理解了count * sizeof(int)做了什么。

ex18.c:38

冒泡排序的外循環(huán)。

ex18.c:39

冒泡排序的內(nèi)循環(huán)。

ex18.c:40

現(xiàn)在我調(diào)用了cmp回調(diào),就像一個普通函數(shù)那樣,但是不通過預先定義好的函數(shù)名,而是一個指向它的指針。調(diào)用者可以像它傳遞任何參數(shù),只要這些參數(shù)符合compare_cb typedef的簽名。

ex18.c:41-43

冒泡排序所需的實際交換操作。

ex18.c:48

最后返回新創(chuàng)建和排序過的結(jié)果數(shù)據(jù)target

ex18.c:51-68

compare_cb函數(shù)類型三個不同版本,它們需要和我們所創(chuàng)建的typedef具有相同的定義。否則C編輯器會報錯說類型不匹配。

ex18.c:74-87

這是bubble_sort函數(shù)的測試。你可以看到我同時將compare_cb傳給了bubble_sort來演示它是如何像其它指針一樣傳遞的。

ex18.c:90-103

一個簡單的主函數(shù),基于你通過命令行傳遞進來的整數(shù),創(chuàng)建了一個數(shù)組。然后調(diào)用了test_sorting函數(shù)。

ex18.c:105-107

最后,你會看到compare_cb函數(shù)指針的typedef是如何使用的。我僅僅傳遞了sorted_order、reverse_orderstrange_order的名字作為函數(shù)來調(diào)用test_sorting。C編譯器會找到這些函數(shù)的地址,并且生成指針用于test_sorting。如果你看一眼test_sorting你會發(fā)現(xiàn)它把這些函數(shù)傳給了bubble_sort,并不關(guān)心它們是做了什么。只要符合compare_cb原型的東西都有效。

ex18.c:109

我們在最后釋放了我們創(chuàng)建的整數(shù)數(shù)組。

你會看到什么

運行這個程序非常簡單,但是你要嘗試不同的數(shù)字組合,甚至要嘗試輸入非數(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 
$

如何使它崩潰

我打算讓你做一些奇怪的事情來使它崩潰,這些函數(shù)指針都是類似于其它指針的指針,他們都指向內(nèi)存的一塊區(qū)域。C中可以將一種指針的指針轉(zhuǎn)換為另一種,以便以不同方式處理數(shù)據(jù)。這些通常是不必要的,但是為了想你展示如何侵入你的電腦,我希望你把這段代碼添加在test_sorting下面:

unsigned char *data = (unsigned char *)cmp;

for(i = 0; i < 25; i++) {
    printf("%02x:", data[i]);
}

printf("\n");

這個循環(huán)將你的函數(shù)轉(zhuǎn)換成字符串,并且打印出來它的內(nèi)容。這并不會中斷你的程序,除非CPU和OS在執(zhí)行過程中遇到了問題。在它打印排序過的數(shù)組之后,你所看到的是一個十六進制數(shù)字的字符串:

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:

這就應該是函數(shù)的原始的匯編字節(jié)碼了,你應該能看到它們有相同的起始和不同的結(jié)尾。也有可能這個循環(huán)并沒有獲得函數(shù)的全部,或者獲得了過多的代碼而跑到程序的另外一片空間。這些不通過更多分析是不可能知道的。

附加題

  • 用十六進制編輯器打開ex18,接著找到函數(shù)起始處的十六進制代碼序列,看看是否能在原始程序中找到函數(shù)。
  • 在你的十六進制編輯器中找到更多隨機出現(xiàn)的東西并修改它們。重新運行你的程序看看發(fā)生了什么。字符串是你最容易修改的東西。
  • 將錯誤的函數(shù)傳給compare_cb,并看看C編輯器會報告什么錯誤。
  • NULL傳給它,看看程序中會發(fā)生什么。然后運行Valgrind來看看它會報告什么。
  • 編寫另一個排序算法,修改test_sorting使它接收任意的排序函數(shù)和排序函數(shù)的比較回調(diào)。并使用它來測試兩種排序算法。