笨辦法學C 練習18:函式指標

飛龍發表於2019-05-12

練習18:函式指標

原文:Exercise 18: Pointers To Functions

譯者:飛龍

函式在C中實際上只是指向程式中某一個程式碼存在位置的指標。就像你建立過的結構體指標、字串和陣列那樣,你也可以建立指向函式的指標。函式指標的主要用途是向其他函式傳遞“回撥”,或者模擬類和物件。在這歌1練習中我們會建立一些回撥,並且下一節我們會製作一個簡單的物件系統。

函式指標的格式類似這樣:

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
", 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
", 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("
");

    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("
");

這個迴圈將你的函式轉換成字串,並且列印出來它的內容。這並不會中斷你的程式,除非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使它接收任意的排序函式和排序函式的比較回撥。並使用它來測試兩種排序演算法。

相關文章