學習目標

  • 會宣告、初始化、走訪一維陣列,理解「索引從 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);          /* 陣列名自動「退化」成首元素的位址 */
}

三個必須先記住的事實(原理在指標單元詳解):

  1. 把陣列名傳給函式時,實際傳的是首元素的位址(一個指標),不是整個陣列的複本。
  2. 因此函式內修改 arr[i] 會改到呼叫端的陣列——這是陣列與一般變數最大的不同!
  3. 函式無法從指標得知陣列長度,所以長度必須用另一個參數傳入。

函式若不打算修改陣列,參數加上 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 的特殊規則。

練習題

  1. 基本題:宣告 10 個元素的 int 陣列存 1~10 的平方,用迴圈填值再反向印出。
  2. 平均與極差:對寫死的成績陣列 { 72, 89, 65, 94, 58, 77 } 求平均(浮點)、最高、最低與極差(最高減最低)。
  3. 反轉陣列:就地(in-place)反轉一個陣列:交換 a[0] 與 a[n-1]、a[1] 與 a[n-2]……印出前後對照(提示:雙索引 i 從頭、j 從尾)。
  4. 次數統計:擲骰子 60 次的結果已寫死在陣列中(值 1~6),統計每個點數出現次數並印出長條圖(提示:counts[value - 1]++,正是 K&R 的映射技巧)。
  5. 選擇排序:泡沫排序的兄弟:每輪從未排序區「選出最小值」放到前面。實作並與泡沫排序比較交換次數。
  6. 挑戰題(去除重複):對已排序的陣列 { 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 陣列