C 語言程式設計教學:函式和指標

PUBLISHED ON AUG 30, 2018 — PROGRAMMING

    在先前的文章中,我們未強調函式和指標的關係,在本文中,我們會介紹函式中和指標相關的部分。

    傳值呼叫 vs. 傳址呼叫

    我們想寫一個將兩數互換的函式,但這樣寫行不通:

    #include <assert.h>
    
    // Useless swap.
    void swap(int, int);
    
    int main(void)
    {
        int a = 3;
        int b = 4;
        
        // No real effect.
        swap(a, b);
        
        // Error!
        assert(a == 4);
        assert(b == 3);
    
        return 0;
    }
    
    void swap(int a, int b)
    {
        int temp = a;
        a = b;
        b = temp;
    }

    這牽涉一些從語法上看不出來的函式的行為。我們這次加上一些額外的訊息:

    #include <assert.h>
    #include <stdio.h>
    
    // Useless swap.
    void swap(int, int);
    
    int main(void)
    {
        int a = 3;
        int b = 4;
    
        fprintf(stderr, "a in main, before swap: %d\n", a);
        fprintf(stderr, "b in main, before swap: %d\n", b);
        
        // No real effect.
        swap(a, b);
        
        fprintf(stderr, "a in main, after swap: %d\n", a);
        fprintf(stderr, "b in main, after swap: %d\n", b);
    
        return 0;
    }
    
    void swap(int a, int b)
    {
        fprintf(stderr, "a in swap, before swap: %d\n", a);
        fprintf(stderr, "b in swap, before swap: %d\n", b);
        
        int temp = a;
        a = b;
        b = temp;
        
        fprintf(stderr, "a in swap, after swap: %d\n", a);
        fprintf(stderr, "b in swap, after swap: %d\n", b);
    }

    印出訊息如下:

    a in main, before swap: 3
    b in main, before swap: 4
    a in swap, before swap: 3
    b in swap, before swap: 4
    a in swap, after swap: 4
    b in swap, after swap: 3
    a in main, after swap: 3
    b in main, after swap: 4
    

    我們發現 abswap 函式中的確有互換,但在函式結束後卻沒有實際的效果。這是因為函式在傳遞參數時,並不是直接將參數傳進去函式內部,而是將其複製一份後傳入。以本例來說,我們只是交換 ab 的複製品而已。

    將函式的參數修改成傳遞指標,如下:

    #include <assert.h>
    #include <stdio.h>
    
    // It really works.
    void swap(int *, int *);
    
    int main(void)
    {
        int a = 3;
        int b = 4;
    
        swap(&a, &b);
        
        assert(a == 4);
        assert(b == 3);
    
        return 0;
    }
    
    void swap(int *a, int *b)
    {
        int temp = *a;
        *a = *b;
        *b = temp;
    }

    這個程式如同我們預期的方式來運作。

    有些 C 語言教材會用傳值呼叫 (pass by value) 和傳址呼叫 (pass by reference) 來區分;但實際上 C 的函式呼叫皆為傳值呼叫,只是在傳遞指標時的「值」是記憶體位址,簡單地說,C 在傳指標時,將指標的位址複製一份後傳入函式內部。

    回傳指標

    我們想寫一個字串相接的函式,但以下程式不會正常運作:

    #include <assert.h>
    #include <stddef.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    // Useless string concat.
    char * my_strcat(char [], char []);
    
    int main()
    {
        char *s = my_strcat("Hello ", "World");
        
        printf("%s\n", s);
        
        return 0;
    }
    
    char * my_strcat(char a[], char b[])
    {
        // Local variable.
        char s[256];
    
        strcat(s, a);
        strcat(s, b);
        
        return s;
    }

    GCC 已經告訴我們原因:

    returnArr.c:34:5: warning: function returns address of local variable [-Wreturn-local-addr]
         return s;
    

    在本例中,my_strcat 的變數 s 從 stack 自動配置記憶體,在函式結束時,s 已經被清除,回傳的值是垃圾值,沒有實質意義。

    修改成以下程式碼即可正常運作:

    #include <assert.h>
    #include <stddef.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    char * my_strcat(char [], char []);
    
    int main()
    {
        char *s = my_strcat("Hello ", "World");
        
        printf("%s\n", s);
        
        free(s);
        
        return 0;
    }
    
    char * my_strcat(char a[], char b[])
    {
        size_t sz_a = strlen(a);
        size_t sz_b = strlen(b);
        size_t sz = sz_a + sz_b + 1;
        
        char *s = calloc(sz, sizeof(char));
    
        strcat(s, a);
        strcat(s, b);
        
        return s;
    }

    在本例中,我們回傳字元指標。由於我們從 heap 配置記憶體,在 my_strcat 函式結束時,記憶體不會清除,故程式可正常運作。只是主函式結束時要記得手動釋放記憶體。

    函式指標

    函式也可以做為型別,透過函式指標可以宣告函式型別。像以下宣告:

    typedef int (*compFn)(int, int);

    透過這個宣告,我們宣告了 compFn 型別,該型別是一個函式,接收兩個整數,回傳一個整數。我們以這個型別寫一個實際的例子:

    #include <assert.h>
    
    typedef int(*compFn)(int, int);
    
    int compute(compFn, int, int);
    int add(int a, int b);
    int sub(int a, int b);
    
    int main()
    {
        assert(compute(add, 3, 4) == 7);
        assert(compute(sub, 3, 4) == -1);
    
        return 0;
    }
    
    int compute(compFn fn, int a, int b)
    {
        return fn(a, b);
    }
    
    int add(int a, int b)
    {
        return a + b;
    }
    
    int sub(int a, int b)
    {
        return a - b;
    }

    在本例中,compute 實際的行為由 fn 決定,我們只要傳入合於 compFn 型別的函式即可改變 compute 的運作方式。雖然 C 語言不是函數式語言,可以透過這項特性做一些高階函式,我們於後續文章會說明。

    我們將上例修改如下:

    #include <assert.h>
    #include <stdbool.h>
    #include <string.h>
    
    typedef int(*compFn)(int, int);
    
    int compute(char [], int, int);
    
    int main()
    {
        assert(compute("+", 3, 4) == 7);
        assert(compute("-", 3, 4) == -1);
    
        return 0;
    }
    
    int add(int a, int b);
    int sub(int a, int b);
    
    int compute(char comp[], int a, int b)
    {
        compFn fn;
    
        if (strcmp(comp, "+") == 0
            || strcmp(comp, "add") == 0) {
            fn = add;        
        }
        else if (strcmp(comp, "-") == 0
            || strcmp(comp, "sub") == 0) {
            fn = sub;        
        }
        else {
            assert("No valid comp" && false);
        }
    
        return fn(a, b);
    }
    
    int add(int a, int b)
    {
        return a + b;
    }
    
    int sub(int a, int b)
    {
        return a - b;
    }

    在這個例子中,變數 fn 的值會隨著傳入的參數而改變,也就是說,函式就像值一樣可傳遞,函數式程式的前提就是建立在此項特性上。

    comments powered by Disqus