在 C 語言中,指標與陣列的關係密不可分:任何用陣列下標能寫的程式,都能用指標改寫。 K&R 甚至說,指標版本通常更快(在現代編譯器下兩者多半等價,但理解這層關係仍是讀懂 C 程式的關鍵)。

學習目標

  • 理解「陣列名在運算式中會轉換成首元素位址」這條核心規則
  • 掌握指標算術:p+1 移動「一個元素」而非一個 byte
  • 會用指標比較(<、==)控制迴圈,用指標走訪陣列
  • 理解為什麼「陣列傳入函式」實際上傳的是指標,以及 sizeof 的陷阱
  • 分辨 char *s = "hello" 與 char s[] = "hello" 的本質差異
  • 能自己用指標實作 strcpy、strcmp、strlen

對照 K&R 章節

本單元主題 K&R 2nd Edition
指標與陣列 §5.3 Pointers and Arrays
位址算術 §5.4 Address Arithmetic
字元指標與函式 §5.5 Character Pointers and Functions

觀念講解

1. 陣列名與指標的關係

宣告 int a[5]; 後,a 在大多數運算式中會自動轉換成 &a[0](首元素的位址):

int a[5] = { 10, 20, 30, 40, 50 };
int *p = a;          /* 等同 int *p = &a[0]; */

printf("%d\n", *p);      /* 10 */
printf("%d\n", a[2]);    /* 30 */
printf("%d\n", *(a+2));  /* 30:a[i] 定義上就是 *(a+i) */
printf("%d\n", p[2]);    /* 30:指標也能用下標! */

記憶體示意圖:

        a[0]    a[1]    a[2]    a[3]    a[4]
       +-------+-------+-------+-------+-------+
       |  10   |  20   |  30   |  40   |  50   |
       +-------+-------+-------+-------+-------+
        ^       ^
        |       |
        p      p+1     (p+1 前進「一個 int」= 4 bytes)

兩個例外:sizeof a 得到整個陣列的大小;&a 得到「指向整個陣列的指標」。 另外,陣列名不是變數:a = p;、a++ 都是非法的。

2. 指標算術

指標加減的單位是「元素」而不是 byte,編譯器會自動乘上 sizeof(元素型別):

int *p = &a[0];
p = p + 1;    /* 位址實際增加 sizeof(int) = 4 */
p++;          /* 同上 */

合法的指標算術:

運算 意義
p + n / p - n 前進 / 後退 n 個元素
p++ / p-- 前進 / 後退一個元素
q - p 兩指標間相隔的元素個數(型別為 ptrdiff_t)
p < q、p == q 比較位置(僅限指向同一陣列時有意義)

注意:兩個指標相加沒有意義,是非法的。

3. 用指標走訪陣列

三種等價寫法:

/* 下標版 */
for (int i = 0; i < n; i++) sum += a[i];

/* 指標 + 計數版 */
int *p = a;
for (int i = 0; i < n; i++) sum += *p++;

/* 雙指標版:p 從頭走到「尾後一格」 */
for (int *p = a; p < a + n; p++) sum += *p;

a + n 指向「最後一個元素的下一格」,C 標準保證這個位址可以參與比較(但不可解參考), 這就是慣用的「尾後指標(one past the end)」寫法。

4. 陣列傳入函式即指標

函式參數中的 int a[] 只是 int *a 的語法糖,大小資訊不會跟著傳進去:

int sum(int a[], int n)      /* 完全等同 int sum(int *a, int n) */
{
    /* 這裡 sizeof a == sizeof(int *),不是整個陣列! */
    int s = 0;
    for (int i = 0; i < n; i++)
        s += a[i];
    return s;
}

所以 C 的慣例是:傳陣列必附長度(或像字串一樣用結尾記號 '\0')。 也因為傳的是指標,函式可以直接修改呼叫端的陣列內容。

5. char *s 與 char s[] 的差異

char amessage[] = "now is the time";   /* 陣列:字元被複製進可修改的陣列 */
char *pmessage  = "now is the time";   /* 指標:指向唯讀的字串字面值 */
amessage: +---+---+---+ ... +---+----+
          | n | o | w | ... | e | \0 |     <- 自己的空間,可修改
          +---+---+---+ ... +---+----+

pmessage: +--------+          唯讀區
          |  addr --+------> "now is the time\0"
          +--------+
  • amessage[0] = 'N'; 合法。
  • pmessage[0] = 'N'; 是未定義行為(字串字面值不可修改,macOS 上會直接當掉)。
  • pmessage = "other"; 合法(指標可以改指向);amessage = ... 非法。

建議:指向字面值的指標一律宣告成 const char *,讓編譯器幫你抓錯。

6. 自己實作字串函式(K&R §5.5)

指標風格的 strcpy,精煉到極致的 C 慣用寫法:

void my_strcpy(char *s, const char *t)
{
    while ((*s++ = *t++) != '\0')
        ;
}

拆解:*t++ 讀出 t 指向的字元後把 t 前進一格;指派給 *s++; 指派運算式的值就是複製的字元,複製到 '\0' 時迴圈結束('\0' 也已複製)。

範例程式

檔案 內容
01_array_name_and_arithmetic.c 陣列名轉指標、a[i] == *(a+i)、指標算術與尾後指標、指標相減
02_array_to_function.c 陣列傳入函式即指標、sizeof 陷阱、函式內修改呼叫端陣列、傳「子陣列」
03_char_pointers_strings.c char * vs char []、走訪字串、自製 strlen/strcpy/strcmp 的下標版與指標版

編譯與執行(以第一個為例):

cc -std=c11 -Wall -Wextra -o /tmp/demo 01_array_name_and_arithmetic.c
/tmp/demo

常見錯誤與陷阱

1. 在函式內用 sizeof 求陣列長度

int count(int a[])
{
    return sizeof a / sizeof a[0];   /* 錯!a 是指標,得到 8/4 = 2 */
}

修正:長度必須由呼叫端算好傳進來:

int n = sizeof data / sizeof data[0];   /* 只在陣列宣告的作用域內有效 */
count(data, n);

2. 修改字串字面值

char *s = "hello";
s[0] = 'H';            /* 未定義行為,macOS 上會 crash */

修正:需要修改就用陣列:

char s[] = "hello";
s[0] = 'H';            /* OK */

3. 指標走出陣列範圍

int a[5];
int *p = a;
while (*p != 0)   /* a 裡若沒有 0,p 會一路衝出界 */
    p++;

修正:用尾後指標當邊界:

for (int *p = a; p < a + 5; p++) { /* ... */ }

4. 對不同陣列的指標做比較或相減

int a[3], b[3];
ptrdiff_t d = &b[0] - &a[0];   /* 未定義行為:不在同一個陣列內 */

只有指向同一陣列(含尾後位置)的指標,比較與相減才有定義。

練習題

  1. 用「雙指標版」迴圈改寫:計算 int 陣列的平均值(不得使用 [] 下標)。
  2. 實作 size_t my_strlen(const char *s),分別寫出下標版與指標相減版 (提示:讓 p 走到 '\0',回傳 p - s)。
  3. 實作 char *my_strchr(const char *s, int c):回傳 c 第一次出現的位置指標,找不到回傳 NULL。
  4. 實作 void reverse_in_place(int *a, int n):用「頭尾雙指標」互換,把陣列就地反轉。
  5. 實作 int my_strend(const char *s, const char *t)(K&R 習題 5-4): 若字串 t 出現在 s 的尾端回傳 1,否則回傳 0。
  6. (進階)實作 void my_strcat(char *s, const char *t),只准用指標、不准用下標。

延伸閱讀

  • K&R 2nd Edition,§5.3–5.5(本單元主軸)
  • comp.lang.c FAQ 第 6 章(Arrays and Pointers):http://c-faq.com/aryptr/
  • 《Expert C Programming》第 4、9、10 章:陣列與指標「不相等」的深入辨析
  • 下一單元 03-multidim-arrays:多維陣列與指標陣列