在 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]; /* 未定義行為:不在同一個陣列內 */
只有指向同一陣列(含尾後位置)的指標,比較與相減才有定義。
練習題
- 用「雙指標版」迴圈改寫:計算
int陣列的平均值(不得使用[]下標)。 - 實作
size_t my_strlen(const char *s),分別寫出下標版與指標相減版 (提示:讓p走到'\0',回傳p - s)。 - 實作
char *my_strchr(const char *s, int c):回傳c第一次出現的位置指標,找不到回傳 NULL。 - 實作
void reverse_in_place(int *a, int n):用「頭尾雙指標」互換,把陣列就地反轉。 - 實作
int my_strend(const char *s, const char *t)(K&R 習題 5-4): 若字串t出現在s的尾端回傳 1,否則回傳 0。 - (進階)實作
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:多維陣列與指標陣列