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

PUBLISHED ON JUL 19, 2018 — PROGRAMMING
FacebookTwitter LinkedIn LINE Skype EverNote GMail Email Email

    指標和陣列的關係相當密切,時常會讓我們以為兩者可相互交換,但兩者仍有些不同。

    相似之處

    指標可以存取陣列的元素:

    #include <assert.h>
    #include <stddef.h>
    
    int main(void)
    {
        int arr[] = {4, 2, 3, 5, 1};
        
        // Same as int *p = &arr[0];
        int *i_p = arr;
        
        for (size_t i = 0; i < 5; i++) {
            assert(*i_p == arr[i]);
            
            // Go to the next element of arr.
            i_p++;
        }
        
        return 0;
    }
    

    在本例中,i_p 指向 arr 第 1 個元素所在的位置。每次 i_p 遞增 1 時,會指向 arr 的下一個位置。這裡利用到指標運算的特性。由於本例的 arr 是靜態配置記憶體,不需手動釋放記憶體。

    我們也可以用指標動態配置一塊陣列:

    #include <assert.h>
    #include <stddef.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int *i_p = calloc(5, sizeof(int));
        if (!i_p) {
            return EXIT_FAILURE;
        }
        
        int arr[] = {4, 2, 3, 5, 1};
        
        for (size_t i = 0; i < 5; i++) {
            i_p[i] = arr[i];
        }
        
        for (size_t i = 0; i < 5; i++) {
            assert(i_p[i] == arr[i]);
        }
        
        // Mutate i_p[1]
        i_p[1] = 99;
        assert(i_p[1] == 99);
        
        free(i_p);
        
        return 0;
    }
    

    在本例中,我們用 calloc 而非 malloc 因為前者可以一併配置記憶體和初始化 i_p。之後的操作其實和一般陣列幾乎沒有兩樣,讀者可自行閱讀相關程式碼。由於 i_p 是從 heap 動態配置記憶體的,最後別忘了要釋放記憶體。

    相異之處

    指標和陣列還是有些不同。像是先前用來估陣列大小的方式在動態配置的陣列會失敗:

    #include <assert.h>
    #include <stddef.h>
    #include <stdlib.h>
    
    int main()
    {
        int *i_p = malloc(sizeof(int) * 5);
        if (!i_p) {
            return EXIT_FAILURE;
        }
        
        // Try to get the size of *i_p.
        size_t sz = sizeof(*i_p) / sizeof(int);
        
        // Failed.
        assert(sz == 5);
        
        free(i_p);
        
        return 0;
    }
    

    我們也不能把陣列變數當指標來運算:

    #include <stddef.h>
    #include <stdio.h>
    
    int main(void) {
        int arr[] = {4, 2, 3, 5, 1};
        
        for (size_t i = 0; i < 5; i++) {
            printf("%d ", *arr);
            
            // Error.
            arr++;
        }
        printf("\n");
        
        return 0;
    }
    

    動態配置多維陣列

    我們先前已經看過動態配置一維陣列的方法,我們這裡來看如何動態配置多維陣列。動態配置多維陣列的方式有兩種:

    • 陣列的陣列 (array of array)
    • 將多維陣列轉為一維陣列

    第一種方式比較直觀,可見下例:

    #include <stddef.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int main(void)
    {
        size_t c = 3;  // Column.
        size_t r = 2;  // Row.
        
        // Allocate outer layer of mtx.
        double **mtx = malloc(r * sizeof(double *));
        if (!mtx) {
            return EXIT_FAILURE;
        }
        
        // Allocate inner layer of mtx.
        for (size_t i = 0; i < r; i++) {
            mtx[i] = calloc(c, sizeof(double));
            if (!mtx[i]) {
                goto FREE;
            }
        }
        
        // Access the element in mtx.
        mtx[1][2] = 9999;
    
    FREE:
        // Free inner layer of mtx.
        for (size_t i = 0; i < r; i++) {
            if (mtx[i]) {
                free(mtx[i]);
            }
        }
    
        // Free outer layer of mtx.
        free(mtx);
    
        return 0;
    }
    

    在本例中,我們配置內外兩層的動態陣列,配置方向是由外向內。外層是由指向 double * 的指標所組成的陣列,外層中每個元素會再由指向 double 的指標配置一個 double 陣列。這裡也用到了 pointer to pointer 的小技巧。

    釋放記憶體時,則是由內向外,和配置時相反。因為我們若把外層指標先釋放了,沒有指標可以存取到內層陣列,這時候再去釋放內層記憶體是未定義行為 (undefined behavior)。

    另一個方法是將其轉為一維陣列:

    #include <stddef.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int main(void)
    {
        size_t c = 3;  // Column.
        size_t r = 2;  // Row.
        
        // Allocate mtx.
        double **mtx = calloc(r * c, sizeof(double));
        if (!mtx) {
            return EXIT_FAILURE;
        }
        
        // Access the element in mtx.
        // Equivalent to mtx[1][2].
        mtx[1*c + 2] = 9999;
    
        // Free mtx.
        free(mtx);
    
        return 0;
    }
    

    這種方法在記憶體配置和釋放上比較簡單,僅有單層記體體區塊要處理。但存取元素時就要將座標稍微轉換一下。第二種方式在效率上會稍微好一些。

    實務上,我們會將這些過程用結構和函式包裝起來,這裡僅是將相關的動作拆解給各位讀者看,比較容易了解。