學習目標
- 會宣告、初始化、走訪一維陣列,理解「索引從 0 開始」與陣列在記憶體中的連續配置。
- 掌握
sizeof arr / sizeof arr[0]計算元素個數的慣用法及其限制。 - 理解「陣列傳入函式時退化為指標」的初步概念——為什麼函式還需要另收一個長度參數。
- 實作基礎演算法:最大值/最小值、線性搜尋、泡沫排序。
- 重現 K&R §1.6 的經典範例:數字頻率統計(digit counting)。
對照 K&R 章節
- K&R §1.6 Arrays(字元分類計數程式)
- 補充:K&R §5.3 Pointers and Arrays(本單元只取「退化」的初步概念)
觀念講解
1. 宣告與初始化
int scores[5]; /* 5 個 int,內容未初始化(垃圾值) */
int primes[5] = { 2, 3, 5, 7, 11 }; /* 完整初始化 */
int partial[5] = { 1, 2 }; /* 前 2 個給值,其餘自動補 0 */
int zeros[100] = { 0 }; /* 慣用法:全部清 0 */
int auto_size[] = { 4, 8, 15 }; /* 讓編譯器數個數:長度 3 */
重點:
- 陣列長度必須是編譯期已知的常數(C89 規則;C99 的 VLA 本教程先不使用)。
- 部分初始化時,剩餘元素保證補 0——這是
{ 0 }全清技巧的原理。 - 沒有初始化的區域陣列內容是垃圾值,跟普通變數一樣。
2. 索引從 0 開始
int a[5] = { 10, 20, 30, 40, 50 };
/* 合法索引:a[0]、a[1]、a[2]、a[3]、a[4] */
/* a[5] 不存在!長度 5 的陣列最後一個元素是 a[4] */
記憶法:索引是「距離開頭幾個元素」。a[0] 距開頭 0 個元素,就是第一個。
陣列元素在記憶體中連續排列,a[i] 的位址 = 開頭位址 + i × sizeof(元素)——
這是索引從 0 開始的真正原因,也是之後理解指標算術的基礎。
3. 走訪與元素個數
int data[] = { 3, 1, 4, 1, 5, 9, 2, 6 };
int n = (int)(sizeof data / sizeof data[0]); /* 總 bytes / 單個 bytes = 個數 */
for (int i = 0; i < n; i++) {
printf("%d ", data[i]);
}
sizeof 慣用法的重要限制:只在「看得到陣列本體」的作用域有效。
陣列一旦傳入函式就退化成指標,sizeof 只會得到指標大小(見下一節)。
4. 陣列與函式:退化為指標(初步概念)
void print_all(const int arr[], int n) /* 參數的 [] 只是給人看的 */
{
/* 在這裡 sizeof arr == sizeof(int *) == 8,不是整個陣列! */
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
}
int main(void)
{
int data[8] = { ... };
print_all(data, 8); /* 陣列名自動「退化」成首元素的位址 */
}
三個必須先記住的事實(原理在指標單元詳解):
- 把陣列名傳給函式時,實際傳的是首元素的位址(一個指標),不是整個陣列的複本。
- 因此函式內修改
arr[i]會改到呼叫端的陣列——這是陣列與一般變數最大的不同! - 函式無法從指標得知陣列長度,所以長度必須用另一個參數傳入。
函式若不打算修改陣列,參數加上 const(如 const int arr[]),讓編譯器幫你把關。
5. 基礎演算法
最大值 / 最小值:先假設第一個是答案,再逐一挑戰:
int max = a[0];
for (int i = 1; i < n; i++) {
if (a[i] > max) {
max = a[i];
}
}
線性搜尋:逐一比對,找到回傳索引、找不到回傳 -1:
int find(const int a[], int n, int target)
{
for (int i = 0; i < n; i++) {
if (a[i] == target) {
return i;
}
}
return -1; /* 慣例:-1 表示「不存在」 */
}
泡沫排序:不斷比較相鄰兩個、順序錯就交換;每一輪把最大值「浮」到最後:
for (int i = 0; i < n - 1; i++) { /* 跑 n-1 輪 */
for (int j = 0; j < n - 1 - i; j++) { /* 每輪少比一個(尾端已排好) */
if (a[j] > a[j + 1]) {
int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp;
}
}
}
泡沫排序是 O(n²) 的教學演算法,實務用 qsort;但它是理解「巢狀迴圈 + 交換」的最佳教材。
6. K&R 數字頻率統計(§1.6)
K&R 用一支程式統計輸入中每個數字(0~9)、空白、其他字元的出現次數,核心技巧是 「用字元值當索引」:
int ndigit[10] = { 0 };
if (c >= '0' && c <= '9') {
ndigit[c - '0']++; /* '7' - '0' == 7:字元差值直接當索引! */
}
c - '0' 把字元 '0'~'9' 映射到整數 0~9——「陣列 + 映射」是計數問題的萬用模式。
範例程式
| 檔案 | 內容 | 編譯與執行 |
|---|---|---|
01_array_basics.c |
宣告、初始化、走訪、sizeof 慣用法、記憶體連續性觀察 | cc -std=c11 -Wall -Wextra -o 01_array_basics 01_array_basics.c && ./01_array_basics |
02_array_functions.c |
陣列傳入函式:退化實證(sizeof 對比)、函式內修改會生效、const 保護 | cc -std=c11 -Wall -Wextra -o 02_array_functions 02_array_functions.c && ./02_array_functions |
03_algorithms.c |
最大/最小值、線性搜尋、泡沫排序(含每輪過程輸出) | cc -std=c11 -Wall -Wextra -o 03_algorithms 03_algorithms.c && ./03_algorithms |
04_digit_count_kr.c |
K&R §1.6 數字頻率統計:用寫死的文字資料統計 + 直方圖輸出 | cc -std=c11 -Wall -Wextra -o 04_digit_count_kr 04_digit_count_kr.c && ./04_digit_count_kr |
常見錯誤與陷阱
陷阱 1:索引越界(off-by-one 的陣列版)
int a[5];
for (int i = 0; i <= 5; i++) { /* i == 5 時越界! */
a[i] = 0;
}
C 不會檢查索引範圍,越界是未定義行為:可能沒事、可能毀掉別的變數、可能當機。
修正:條件用 i < 5。長度 n 的陣列,合法索引是 0 到 n-1。
陷阱 2:在函式內用 sizeof 算陣列長度
void process(int arr[])
{
int n = sizeof arr / sizeof arr[0]; /* 錯:8 / 4 = 2,永遠是 2! */
}
參數 arr 實際是指標,sizeof arr 是指標大小(8)。修正:長度由呼叫端算好傳入。
-Wall 之下 clang 會警告 sizeof on array function parameter。
陷阱 3:陣列不能整批指定或比較
int a[3] = { 1, 2, 3 };
int b[3];
b = a; /* 編譯錯誤:陣列名不是可指定的左值 */
if (a == b) { ... } /* 合法但錯誤:比較的是「位址」不是內容! */
修正:複製用迴圈(或 memcpy);比較內容用迴圈逐一比對(或 memcmp)。
陷阱 4:忘記「部分初始化補 0、完全不初始化是垃圾」的差別
int counts[10]; /* 區域陣列:內容是垃圾值 */
counts[3]++; /* 垃圾 + 1 = 垃圾:計數全毀 */
修正:計數陣列一定要 int counts[10] = { 0 };。
陷阱 5:用變數當長度宣告後又假設它是固定長度
int n = 5;
int arr[n]; /* C99 VLA:可以編譯,但無法初始化、無法 sizeof 慣用 */
int arr2[n] = { 0 }; /* 編譯錯誤:VLA 不能初始化 */
初學階段建議:長度一律用 #define 或 enum 常數,避免 VLA 的特殊規則。
練習題
- 基本題:宣告 10 個元素的 int 陣列存 1~10 的平方,用迴圈填值再反向印出。
- 平均與極差:對寫死的成績陣列
{ 72, 89, 65, 94, 58, 77 }求平均(浮點)、最高、最低與極差(最高減最低)。 - 反轉陣列:就地(in-place)反轉一個陣列:交換
a[0]與a[n-1]、a[1]與a[n-2]……印出前後對照(提示:雙索引i從頭、j從尾)。 - 次數統計:擲骰子 60 次的結果已寫死在陣列中(值 1~6),統計每個點數出現次數並印出長條圖(提示:
counts[value - 1]++,正是 K&R 的映射技巧)。 - 選擇排序:泡沫排序的兄弟:每輪從未排序區「選出最小值」放到前面。實作並與泡沫排序比較交換次數。
- 挑戰題(去除重複):對已排序的陣列
{ 1, 1, 2, 3, 3, 3, 5, 8, 8 }就地去重,印出去重後的有效長度與內容(提示:讀寫雙索引)。
延伸閱讀
- K&R 2/e §1.6、§1.9;習題 1-13(單字長度直方圖)
- K&R 2/e §5.3「Pointers and Arrays」——退化機制的完整解釋(高階單元預習)
- 《Programming Pearls》第 2 章:陣列旋轉問題——陣列操作的思維體操
- C11 標準 §6.7.6.3:「array of T」參數如何被調整為「pointer to T」
- 下一單元預告:字元與字串——特殊而重要的 char 陣列