開放原始碼技術文件網 如何使用陣列 (Array)

前言

陣列是線性且同質的資料結構,使用零或正整數為索引來存取其中元素。在 C 語言中,陣列是唯一的內建資料結構,其他的動態資料結構需自行實作。本文介紹陣列的使用方式。

宣告陣列

以下敘述建立一個長度為 5、元素型別為 int 的陣列 arr

int arr[5];

要注意這時候陣列元素尚未初始化。陣列未初始化時所存的值視為垃圾值,其運算結果不可靠。

我們也可以在宣告陣列時一併賦值:

int arr[5] = {3, 4, 5, 6, 7};

或者是用稍微取巧的方式來初始化:

int arr[] = {3, 4, 5, 6, 7};

這時候陣列 arr 的長度由賦值自動決定,在本範例敘述中即為 5

如果想要明確地列出陣列元素所在的位置,可以用下列方式來宣告陣列:

int arr[] = {
    [0] = 3,
    [1] = 4,
    [2] = 5,
    [3] = 6,
    [4] = 7,
};

這樣寫程式碼會變長,好處是可明確看出每個元素所在的位置。

但要注意陣列的長度不能由變數決定:

int main(void)
{
    unsigned sz = 5;
        
    // Error.
    int arr[sz] = {1, 2, 3, 4, 5};

    return 0;
}

因為陣列的長度要在編譯期就決定好。如果想要在執行期動態生成陣列,要用動態配置記憶體的方式。我們後文會談如何動態配置陣列。

存取陣列元素

陣列使用零或正整數存取陣列元素。參考以下範例:

#include <assert.h>         /*  1 */

int main(void)              /*  2 */
{                           /*  3 */
    int arr[] = {3, 4, 5};  /*  4 */

    assert(arr[0] == 3);    /*  5 */
    assert(arr[1] == 4);    /*  6 */
    assert(arr[2] == 5);    /*  7 */

    return 0;               /*  8 */
}                           /*  9 */

我們在第 3 行宣告了長度為 3,元素型別為 int 的陣列 arr。然後在第 5 行至第 7 行間分別對其中元素以索引取值。利用斷言確認取出的值是正確的。

注意取索引時,第一個元素的索引值從 0 開始,而非 1,這是因為索引是一種偏移值 (offset) 的概念。

但 C 語言不會檢查索引是否逾越陣列的邊界。參考以下反例:

#include <stdio.h>                        /*  1 */

int main(void)                            /*  2 */
{                                         /*  3 */
    int arr[] = {3, 4, 5};                /*  4 */

    printf("%d\n", arr[3]);  /* Error */  /*  5 */

    return 0;                             /*  6 */
}                                         /*  7 */

陣列 arr 的長度為 3,最大的合法索引值應為 2,但本例在第 5 行時故意取索引值為 3 的值。這時候取出的值是垃圾值,其運算結果不可靠。

由於逾越邊界 (out of bound) 算是常見的錯誤,資訊界出現過數個 C 方言 (C dialect),意圖改善 C 常見的錯誤。其中一個例子是微軟的研究項目 Checked C。但這些 C 方言,除了展示一些對 C 語言的想法外,幾乎沒用程式人將其在實務上。

如果讀者真的很在意陣列邊界的問題,現階段的方式就是自行實作工具函式或陣列物件,在這些自製函式或物件中加入邊界檢查的功能。

走訪陣列

走訪陣列元素的方式是使用 for 迴圈搭配計數器 (counter)。參考下例:

#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int arr[] = {3, 4, 5, 6, 7};
    
    for (size_t i = 0; i < 5; i++) {
        printf("%d\n", arr[i]);   
    }

    return 0;
}

這裡我們把計數器的邊界寫死在程式中,實務上不會用這樣的方式。取得陣列大小的方式請看下一節。

計算陣列大小

C 陣列本身沒有儲存陣列大小的資訊。如果想要知道陣列的大小,得自行計算。參考下例:

#include <assert.h>                         /*  1 */
#include <stddef.h>                         /*  2 */

int main(void)                              /*  3 */
{                                           /*  4 */
    int arr[] = {3, 4, 5, 6, 7};            /*  5 */

    size_t sz = sizeof(arr) / sizeof(int);  /*  6 */

    assert(sz == 5);                        /*  7 */

    return 0;                               /*  8 */
}                                           /*  9 */

在此範例程式中,我們在第 6 行分別計算陣列大小和陣列元素大小,將其相除後即可得陣列長度。在本例中其值為 5

但這個方式只對自動配置記憶體的陣列有效,若陣列使用動態配置記憶體,則無法使用這個方法。

如果我們想儲存陣列長度的資訊,需將長度存在另一個變數中。由於陣列和陣列長度兩者是連動的,我們會用結構體把兩個變數包在一起。例如,以下結構體宣告一個動態陣列:

struct array_t {
    size_t size;
    size_t capacity;
    int *elems;
};

我們會在後文介紹結構體。至於實作動態陣列的方式,則需參考資料結構方面的教材。筆者也有寫關於動態陣列的文章,有興趣的讀者可以看一下。

動態配置的陣列

我們先前的範例中,陣列使用自動配置記憶體。但我們若要在執行期動態生成陣列,則要改用動態配置記憶體的方式。

我們同樣用 malloc() 函式來配置記憶體。參考以下敘述:

int *arr = (int *) malloc(sz * sizeof(int));

我們以 sizeof 求得單一元素的大小後,乘上陣列的長度 sz 即可配置一塊足夠大小的記憶體,用來儲存陣列 arr 的元素。由此可知,陣列在電腦中以是一整塊連續的記憶體來儲存,所以可以用索引值快速存取。

如果想要在配置記憶體時一併將元素初始化為 0,改用 calloc() 函式即可。但 calloc() 函式的參數略有不同:

int *arr = (int *) calloc(sz, sizeof(int));

由於多了初始化的動作,calloc() 函式會比 malloc() 函式慢一點點。

使用完後同樣要釋放記憶體:

free(arr);

由於陣列內部是單一且連續的記憶體區塊,所以可在單一 free() 函式呼叫中釋放掉。不論使用 malloc()calloc(),皆使用 free() 來釋放記憶體。

我們來看一個動態配置記憶體陣列的範例:

#include <stdlib.h>                                                       /*  1 */
#include <stdio.h>                                                        /*  2 */

int main(void)                                                            /*  3 */
{                                                                         /*  4 */
    size_t sz = 5;                                                        /*  5 */

    int *arr = (int *) malloc(sz * sizeof(int));                          /*  6 */
    if (!arr) {                                                           /*  7 */
        perror("Failed to allocate an array\n");                          /*  8 */
        goto ERROR;                                                       /*  9 */
    }                                                                     /* 10 */

    for (size_t i = 0; i < sz; i++) {                                     /* 11 */
        arr[i] = 3 + i;                                                   /* 12 */
    }                                                                     /* 13 */
    
    int data[] = {3, 4, 5, 6, 7};                                         /* 14 */
    for (size_t i = 0; i < sizeof(data) / sizeof(int); i++) {             /* 15 */
        if (arr[i] != data[i]) {                                          /* 16 */
            fprintf(stderr, "Unequal values: %d %d\n", arr[i], data[i]);  /* 17 */
            goto ERROR;                                                   /* 18 */
        }                                                                 /* 19 */
    }                                                                     /* 20 */

    free(arr);                                                            /* 21 */

    return 0;                                                             /* 22 */

ERROR:                                                                    /* 23 */
    if (arr)                                                              /* 24 */
        free(arr);                                                        /* 25 */

    return 1;                                                             /* 26 */
}                                                                         /* 27 */

我們在第 6 行動態配置陣列 arr。由於動態配置記憶體可能失敗,我們在第 7 行至第 10 行間檢查 arr 是否存在。當配置記憶體未成功時,放棄一般的程式流程,改走錯誤處理流程。

接著,我們在第 11 行至第 13 行間對 arr 的元素逐一賦值。

我們在第 14 行至第 20 行間檢查 arr 的值是否正確,若發生錯誤,印出錯誤值並中斷一般程式流程。

最後,在第 21 行釋放掉 arr 所占用的記憶體。

多維陣列

先前的範例皆為一維陣列,但 C 語言允許多維陣列。參考以下宣告多維陣列的敘述:

int mtx[3][2] = {
    {1, 2},
    {3, 4},
    {5, 6}
};

我們同樣可以對多維陣列存取索引值:

#include <assert.h>

int main(void)
{
    int mtx[3][2] = {
        {1, 2},
        {3, 4},
        {5, 6}
    };

    assert(mtx[1][1] == 4);

    return 0;
}

上述範例的記憶體是自動配置的。那如果我們要動態配置記憶體呢?這時候有兩種策略,其中一種較直觀的策略是動態配置陣列的陣列。參考以下範例:

#include <stdio.h>                                               /*  1 */
#include <stdlib.h>                                              /*  2 */

int main(void)                                                   /*  3 */
{                                                                /*  4 */
    size_t row = 3;                                              /*  5 */
    size_t col = 2;                                              /*  6 */

    int **mtx = (int **) malloc(row * sizeof(int *));            /*  7 */
    if (!mtx) {                                                  /*  8 */
        perror("Failed to allocate rows of mtx\n");              /*  9 */
        goto ERROR;                                              /* 10 */
    }                                                            /* 11 */

    for (size_t i = 0; i < row; i++) {                           /* 12 */
        mtx[i] = (int *) malloc(col * sizeof(int));              /* 13 */
        if (!mtx[i]) {                                           /* 14 */
            fprintf(stderr, "Failed to allocate col of mtx\n");  /* 15 */
            goto ERROR;                                          /* 16 */
        }                                                        /* 17 */
    }                                                            /* 18 */

    int n = 1;                                                   /* 19 */
    for (size_t i = 0; i < row; i++) {                           /* 20 */
        for (size_t j = 0; j < col; j++) {                       /* 21 */
            mtx[i][j] = n;                                       /* 22 */

            n++;                                                 /* 23 */
        }                                                        /* 24 */
    }                                                            /* 25 */

    /* {{1, 2},
        {3, 4},
        {5, 6}} */
    if (!(mtx[1][1] == 4)) {                                     /* 26 */
        fprintf(stderr, "Wrong value: %d\n", mtx[1][1]);         /* 27 */
        goto ERROR;                                              /* 28 */
    }                                                            /* 29 */

    for (size_t i = 0; i < row; i++) {                           /* 30 */
        free(mtx[i]);                                            /* 31 */
    }                                                            /* 32 */

    free(mtx);                                                   /* 33 */

    return 0;                                                    /* 34 */

ERROR:                                                           /* 35 */
    if (mtx) {                                                   /* 36 */
        for (size_t i = 0; i < row; i++) {                       /* 37 */
            if (mtx[i])                                          /* 38 */
                free(mtx[i]);                                    /* 39 */
        }                                                        /* 40 */

        free(mtx);                                               /* 41 */
    }                                                            /* 42 */

    return 1;                                                    /* 43 */
}                                                                /* 44 */

本範例的多維陣列的型別是 int **。這其實是指向指標的指標。第一層指標是指向 int * 的指標,第二層指標則是指向 int 的指標。

我們在第 7 行配置第一層陣列,其長度為 row,陣列元素的大小為 int *

我們在第 12 行至第 18 行間配置第二層陣列,其長度為 col,陣列元素的大小為 int。由於我們要配置多次,故我們使用迴圈來重覆進行相同的任務。

接著,我們在第 19 行至第 25 行間用雙層迴圈逐一對多維陣列 mtx 賦值。

本範例為了簡化,只在第 26 行至第 28 行間檢查其中一個元素。如果讀者有興趣,可試著改寫這個程式,改成對每個元素逐一檢查。

最後在第 30 行至 33 行間釋放記憶體。注意由內而外逐一釋放。若我們先釋放外部陣列,就沒有合法的指標可指向內部陣列的記憶體,造成記憶體洩露。

我們先前有提過,多維陣列有兩種宣告方式。除了前述的陣列的陣列外,我們仍然可以用一維陣列儲存多維陣列,再將外部多維座標經計算轉換為內部一維座標。參考以下結構體宣告:

struct matrix_t {
    size_t row;
    size_t col;
    double *elements;
};

在這個結構體中,我們內部使用一維陣列 elements 來儲存陣列元素。要存取陣列元素時得將座標進行轉換。至於實作的方式,屬於資料結構的範圍,此處不詳談。

電子書籍

如果你覺得這篇 C 語言的技術文章對你有幫助,可以看看以下完整的 C 語言程式設計電子書:

現代 C 語言程式設計

分享本文
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
追蹤本站
Facebook Facebook Twitter