寫 C 寫了好幾年,我一直以為自己搞懂指標跟陣列的關係了。
直到有一次,我在一個跨檔案的專案裡,某個 .c 檔宣告了 char buf[256],另一個 .c 檔用 extern char *buf 去引用它。編譯過了,連結也過了,一跑就 segfault。
我盯著螢幕看了十分鐘,心裡想:「這兩個不是一樣的東西嗎?」
不是。這篇就是要把這件事從根本講清楚。
🧠 陣列不是指標,指標不是陣列
先把結論放前面:陣列和指標是兩種完全不同的東西,只是在某些場景下行為很像,像到讓人誤以為它們是同一個東西。
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;
這兩行之後,arr[2] 跟 ptr[2] 的值一樣,都是 30。用起來感覺完全一樣對吧?但底層完全不同:
arr是一塊連續的記憶體空間,佔5 * sizeof(int)個 byte,它本身就是那塊記憶體ptr是一個變數,裡面存的是一個位址,佔sizeof(int *)個 byte(通常 4 或 8 bytes)
一個是「那塊地」,一個是「寫著地址的紙條」。你拿著紙條可以找到那塊地,但紙條本身不是那塊地。
🔄 Array Decay — 那個讓人搞混的元凶
為什麼大家會搞混?因為 C 語言有一個規則叫做 array decay(陣列退化):在大多數 expression 裡,陣列名稱會自動轉換成指向第一個元素的指標。
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // arr 在這裡 decay 成 &arr[0]
這個轉換是隱式的、自動的、無聲無息的。所以你寫 ptr = arr 的時候,感覺好像在說「指標就是陣列」,但其實是 compiler 偷偷幫你轉了。
但有三個地方不會 decay:
sizeof(arr) // 回傳整個陣列的大小,不是指標的大小
&arr // 取得「整個陣列」的位址,型別是 int (*)[5]
// 以及 arr 作為 string literal 初始化 char[] 的時候
這三個例外超級重要。特別是 sizeof,這是區分陣列跟指標最直接的方法。
📏 sizeof 是照妖鏡
這個是我覺得最能說明兩者差異的地方:
int arr[5];
int *ptr = arr;
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 20(假設 int 是 4 bytes)
printf("sizeof(ptr) = %zu\n", sizeof(ptr)); // 8(64-bit 系統)
sizeof(arr) 回傳的是整個陣列佔的空間,sizeof(ptr) 回傳的是一個指標變數的大小。這就是鐵證——它們根本不是同一種東西。
這也是為什麼系列裡另一篇 sizeof 的陷阱 會專門討論「把陣列傳進函式之後 sizeof 就不對了」的問題。因為一旦 decay 發生,陣列的大小資訊就永遠丟失了。
🚨 函式參數裡的陣列:全是假象
這是最多人踩坑的地方。你寫:
void process(int data[10]) {
printf("sizeof = %zu\n", sizeof(data)); // 8,不是 40!
}
那個 int data[10] 長得像陣列參數,但 compiler 實際上把它當成 int *data。方括號裡的 10?compiler 完全無視。你寫 int data[10] 跟寫 int data[999] 跟寫 int *data,對 compiler 來說一模一樣。
這不是 bug,是 C 語言的設計。C standard 規定:函式參數裡的陣列宣告會被「調整」(adjusted)為指標。
所以如果你需要在函式裡知道陣列的長度,必須另外傳:
void process(int *data, size_t len) {
for (size_t i = 0; i < len; i++) {
// ...
}
}
// 呼叫的時候
int buf[10];
process(buf, sizeof(buf) / sizeof(buf[0]));
我自己會用一個 macro 來避免每次都手寫 sizeof 除法:
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
// 用法
process(buf, ARRAY_SIZE(buf));
不過要小心,這個 macro 如果對指標用,不會報錯但會給你一個無意義的值。Linux kernel 有一個更安全的版本 ARRAY_SIZE 會在 compile time 檢查參數是不是真的陣列,但那個實作比較複雜,這裡先不展開。
💣 跨檔案宣告的大坑
回到開頭的故事。為什麼 char buf[256] 跟 extern char *buf 搭配會爆?
假設在 file_a.c 裡:
// file_a.c
char buf[256] = "hello";
然後在 file_b.c 裡:
// file_b.c
extern char *buf; // ❌ 錯誤!
void foo(void) {
printf("%s\n", buf); // 💥 segfault 或亂碼
}
問題在哪?
buf 在 file_a.c 裡是一個 256 byte 的陣列,起始位址假設是 0x1000,前幾個 byte 的內容是 'h', 'e', 'l', 'l', 'o', '\0'...
但 file_b.c 裡 extern char *buf 告訴 compiler:「buf 是一個指標變數」。所以 compiler 會去 0x1000 讀取一個指標大小的值(8 bytes),把 'h', 'e', 'l', 'l', 'o', '\0', '\0', '\0' 這 8 個 byte 解讀成一個記憶體位址。
然後它就跑去那個莫名其妙的位址讀資料了。當然爆。
正確的做法:
// file_b.c
extern char buf[]; // ✅ 宣告為不完整陣列型別
// 或
extern char buf[256]; // ✅ 大小要跟定義一致
這個 bug 特別陰險的地方是:編譯不會報錯,連結不會報錯,只有跑起來才會炸。而且因為讀到的那個「假位址」有時候碰巧指向合法記憶體,所以不一定每次都 crash,可能只是偶爾出現亂碼。在嵌入式系統上 debug 這種問題會讓人懷疑人生。
我那次花了半天才找到原因,之後養成習慣:陣列的 extern 宣告統一放在 header 檔,只寫一次,然後所有 .c 檔都 include 同一個 header。這樣至少 compiler 能幫你抓型別不一致。
🧮 指標算術 vs 陣列索引
arr[i] 跟 *(arr + i) 在 C 裡面是完全等價的。不是「行為類似」,是根據 C standard 的定義,arr[i] 就等於 *(arr + i)。
這也是為什麼你可以寫出這種奇怪但合法的 code:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", 2[arr]); // 輸出 30 😂
因為 2[arr] 被解讀成 *(2 + arr),而加法有交換律,所以跟 *(arr + 2) 一樣。當然,拜託不要在 production code 裡這樣寫。
但指標算術有一個常見的誤解:ptr + 1 不是位址加 1,而是位址加 sizeof(*ptr)。
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;
printf("ptr = %p\n", (void *)ptr); // 假設 0x1000
printf("ptr+1 = %p\n", (void *)(ptr + 1)); // 0x1004,不是 0x1001
Compiler 會根據指標指向的型別自動計算偏移量。這在處理 uint8_t buffer 跟 uint32_t buffer 的時候差別很大,搞混的話就會讀到錯誤的位置。
📦 多維陣列與指標的對應
二維陣列的指標關係又更繞了一層:
int matrix[3][4];
matrix 是一個「3 個元素的陣列,每個元素是 4 個 int 的陣列」。當它 decay 的時候,不會變成 int *,而是 int (*)[4]——指向「4 個 int 的陣列」的指標。
int matrix[3][4];
int (*row_ptr)[4] = matrix; // ✅ 正確
int *flat_ptr = matrix; // ❌ 型別不匹配
int *flat_ptr = matrix[0]; // ✅ 第一列 decay 成 int*
這在嵌入式裡蠻常碰到的。比如你有一個 sensor 資料表,每個 sensor 有固定數量的 channel:
#define NUM_SENSORS 4
#define NUM_CHANNELS 8
uint16_t sensor_data[NUM_SENSORS][NUM_CHANNELS];
// 要傳某一個 sensor 的所有 channel 給函式
void process_sensor(uint16_t *channels, size_t num_ch);
// 呼叫的時候
process_sensor(sensor_data[2], NUM_CHANNELS); // 傳第 3 個 sensor 的資料
sensor_data[2] 本身是一個 uint16_t[8] 的陣列,decay 之後變成 uint16_t *,型別剛好對得上。但如果你搞錯維度的對應關係,讀到的就是別的 sensor 的資料了。
🔑 &arr 跟 arr 值一樣,但型別不同
最後一個容易搞混的:
int arr[5];
printf("%p\n", (void *)arr); // 假設 0x1000
printf("%p\n", (void *)&arr); // 也是 0x1000
值一樣,但型別完全不同:
arr(decay 之後)的型別是int *,指向第一個元素&arr的型別是int (*)[5],指向整個陣列
差別在指標算術上:
printf("%p\n", (void *)(arr + 1)); // 0x1004,跳 4 bytes(一個 int)
printf("%p\n", (void *)(&arr + 1)); // 0x1014,跳 20 bytes(整個陣列)
這在實務上比較少直接用到,但如果你在讀別人的 code 或分析 compiler 錯誤訊息的時候,知道這個區別會讓你少走很多冤枉路。
整理一下
把幾個最常搞混的情境列出來:
int arr[5];
int *ptr = arr;
// 這些是一樣的
arr[2] ≡ *(arr + 2) ≡ ptr[2] ≡ *(ptr + 2)
// 這些不一樣
sizeof(arr) → 20 // 整個陣列的大小
sizeof(ptr) → 8 // 指標變數的大小
// 這個會出事
extern int *arr; // ❌ 如果原始定義是 int arr[5]
extern int arr[]; // ✅ 正確的跨檔案宣告
// 函式參數裡
void foo(int arr[5]) ≡ void foo(int *arr) // compiler 不在乎方括號裡的數字
寫 C 這麼多年,我覺得指標跟陣列的關係就是那種「你以為懂了,但隔幾個月碰到一個奇怪的 case 又開始懷疑自己」的東西。特別是在嵌入式開發裡,pointer arithmetic 跟 memory layout 關係密切,搞錯一個 offset 可能不是 crash,而是靜靜地把隔壁的變數覆蓋掉,讓你 debug 到天荒地老。
把 decay 的規則記住,把 sizeof 當照妖鏡用,extern 宣告統一放 header——做到這三件事,至少可以避開八成的坑。
✅ 這篇的知識點
- 陣列是一塊記憶體,指標是存放位址的變數——一個是「地」,一個是「紙條」
- Array decay:陣列名稱在大多數 expression 裡會隱式轉換成指向首元素的指標
sizeof、&運算子、字串初始化是 decay 的三個例外sizeof(arr)回傳整個陣列大小,sizeof(ptr)回傳指標大小——這是照妖鏡- 函式參數裡的
int arr[10]會被 compiler 調整為int *arr,方括號裡的數字沒有意義 - 需要在函式裡知道陣列長度,就額外傳一個
size_t len參數 ARRAY_SIZE(arr)macro 可以簡化sizeof(arr)/sizeof(arr[0]),但對指標無效- 跨檔案引用陣列要用
extern int arr[],不能用extern int *arr——型別不同,會 segfault extern宣告統一放 header 檔,讓 compiler 幫你抓型別不一致ptr + 1跳的是sizeof(*ptr)個 byte,不是 1 個 byte- 二維陣列 decay 成的是「指向一維陣列的指標」(如
int (*)[4]),不是int * arr跟&arr值相同但型別不同,指標算術的步進大小完全不一樣