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