[C 語言] 程式設計教學:如何使用陣列 (Array)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
【贊助商連結】

    C 語言的陣列 (array) 是一種線性 (linear)、同質 (homogeneous)、連續的 (contiguous) 容器 (collection) 或資料結構 (data structure),陣列包括兩個要素:

    • 陣列元素 (element) 的資料型別
    • 陣列的大小

    C 語言的陣列元素一定要相同型別,所以是同質的。由於 C 的陣列不儲存長度的資訊,程式設計者需預先指定或用另一個變數儲存陣列長度。本文會介紹 C 語言陣列的基本使用方式。

    宣告陣列

    以下範例程式展示幾種宣告陣列的方式:

    int main(void)
    {
        {
            // Declare an array without init.
            int arr[5];
        }
        
        {
            // Init an array with pre-defined array length.
            int arr[5] = {1, 2, 3, 4, 5};
        }
        
        {
            // Init an array and decide the array length by assignment.
            int arr[] = {1, 2, 3, 4, 5};
        }
    
        return 0;
    }

    在第一種方式中,我們宣告了一個大小為 5,儲存 int 的陣列 arr,但沒有將陣列初始化,這時候陣列內的值是無意義的垃圾值。

    在第二種方式中,我們除了宣告陣列 arr 外,還將其初始化。

    在第三種方式中,算是稍微簡略的寫法,由編譯器自動推算 arr 的大小。

    但陣列大小要在編譯期就決定其大小,使用變數宣告陣列大小時會引發錯誤,參考以下實例:

    #include <stddef.h>
    
    int main(void)
    {
        size_t sz = 5;
            
        // Error.
        int arr[sz] = {1, 2, 3, 4, 5};
    
        return 0;
    }

    錯誤訊息如下:

    error: variable-sized object may not be initialized

    這是因為自動配置的陣列要預先知道其大小,如果要在程式中動態決定陣列的大小,需藉由動態記憶體配置的方式來達成。

    存取陣列元素

    陣列以大於等於 0 的整數為索引 (index),第一個元素位於 0,即 zero-based。見以下實例:

    #include <assert.h>
    
    int main(void)
    {
        int arr[] = {4, 3, 5, 1, 2};
        
        assert(arr[0] == 4);
        assert(arr[1] == 3);
        assert(arr[2] == 5);
        assert(arr[3] == 1);
        assert(arr[4] == 2);
    
        return 0;
    }

    日常生活的計數是由 1 開始,但大部分程式語言的陣列都是 zero-based,寫一陣子程式就會習慣這種方式。一個簡單的想法是將索引想成偏移值 (offset):

    • 第 1 個元素沒有偏移,故索引值為 0
    • 第 2 個元素偏移 1 格,故索引值為 1
    • 第 3 個元素偏移 2 格,故索引值為 2
    • 其他元素以此類推

    除了取出值,也可以寫入值。如下例:

    #include <assert.h>
    
    int main(void)
    {
        int arr[] = {2, 4, 1, 3};
        
        assert(arr[0] == 2);
        assert(arr[1] == 4);
        assert(arr[2] == 1);
        assert(arr[3] == 3);
     
        // Mutate the value on arr[1]
        arr[1] = 99;
        
        assert(arr[0] == 2);
        assert(arr[1] == 99);
        assert(arr[2] == 1);
        assert(arr[3] == 3);
    
        return 0;
    }

    存取超過陣列大小的元素是未定義行為 (undefined behavior),所謂的未定義行為是指未規範在 C 標準的情境,由各個 C 編譯器自行處理。如以下實例:

    #include <stdio.h>
    
    int main()
    {
        int arr[] = {4, 2, 1, 3};
        
        // Undefined behavior.
        printf("%d\n", arr[4]);
        
        return 0;
    }

    在本例中,我們存取的位置已經越界 (out of bounds) 了,不論 arr[4] 取得的值是什麼,都沒有實質的意義,只是一些垃圾值。

    走訪陣列

    C 的陣列沒有內建的迭代器 (iterator),直接以索引值走訪即可:

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

    記算陣列的大小

    C 語言的陣列本身不會儲存陣列的大小,不過可以用 sizeof 計算出來。見下例:

    #include <assert.h>
    #include <stddef.h>
    
    int main(void) {
        int arr[] = {4, 2, 3, 5, 1};
        
        // Calculate the size of arr.
        size_t sz = sizeof(arr) / sizeof(int);
        
        assert(sz == 5);
    
        return 0;
    }

    但這僅限於靜態配置或自動配置記憶體的陣列。比較實用的方法是將陣列大小的資訊另外儲存,如下例:

    #include <stddef.h>
    
    // `array_t` is a dynamic array, i.e. a resizable array.
    typedef struct array_t array_t;
    
    struct array_t {
        size_t size;
        size_t capacity;
        double *elements;
    };
    
    // Implement `array_t` here.

    在此例中,我們將陣列包在一個結構中,當我們需要陣列大小時,就取 size 的值即可。上述程式碼的內容超出目前的範圍,這是學完 C 的語法後,開始練習用 C 實作資料結構時才會碰到的情境。

    多維陣列

    前述的陣列是一維陣列,C 也支援多維陣列 (multi-dimensional array),如下例:

    #include <assert.h>
    
    int main(void)
    {
        int mtx[2][3] = {
            {1, 2, 3},
            {4, 5, 6}
        };
        
        assert(mtx[1][2] == 6);
        
        return 0;
    }

    不過,這種方法會把陣列的大小寫死,實用性偏低。如果我們要用 C 實作數學上的向量 (vector) 或矩陣 (matrix),比較少用這種方法,而會採下列方式來宣告:

    #include <stddef.h>
    
    // `matrix_t` is a 2D matrix.
    typedef struct matrix_t matrix_t;
    
    struct matrix_t {
        size_t col;
        size_t row;
        double *elements;
    };
    
    // Implement `matrix_t` here.

    這裡用到結構 (struct) 和指標 (pointer) 等語法,一開始看不懂是正常的,這是學完 C 的核心語法後,開始練習用 C 實作資料結構時才會碰到的情境。

    陣列和指標相似之處

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

    #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]);
            
            // Iterate 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;
    }
    

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

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

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【支持站長】
    Buy me a coffeeBuy me a coffee
    【贊助商連結】
    標籤: C 語言, ARRAY
    【分類瀏覽】