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

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    陣列是線性且同質的資料結構,使用零或正整數為索引來存取其中元素。在 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 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 來儲存陣列元素。要存取陣列元素時得將座標進行轉換。至於實作的方式,屬於資料結構的範圍,此處不詳談。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk
    標籤: ARRAY, C 語言