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