二維陣列和「指標陣列」長得很像(都能寫 x[i][j]),底層卻是完全不同的兩種結構。
本單元釐清它們的記憶體佈局、傳入函式的正確寫法,並重現 K&R 的月份名稱經典範例。
學習目標
- 理解二維陣列的 row-major(列優先) 記憶體佈局
- 會把二維陣列傳入函式,並理解「為什麼欄數不能省略」
- 掌握指標陣列(如
char *lines[])的結構與用途 - 理解「指標的指標」(
char **)以及它與指標陣列的關係 - 能清楚比較「指標陣列」與「二維陣列」的差異與適用場景
- 看懂並能默寫 K&R 的
month_name範例
對照 K&R 章節
| 本單元主題 | K&R 2nd Edition |
|---|---|
| 多維陣列(日期轉換範例) | §5.7 Multi-dimensional Arrays |
| 指標陣列與指標的指標 | §5.6 Pointer Arrays; Pointers to Pointers |
| 指標陣列的初始化(month_name) | §5.8 Initialization of Pointer Arrays |
| 指標 vs 多維陣列 | §5.9 Pointers vs. Multi-dimensional Arrays |
觀念講解
1. 二維陣列的記憶體佈局(row-major)
int m[3][4] 是「3 個元素的陣列,每個元素是 4 個 int 的陣列」。
記憶體中一整列接著一整列連續排列:
m[0][0] m[0][1] m[0][2] m[0][3] m[1][0] m[1][1] ... m[2][3]
+------+------+------+------+------+------+-----+------+
| 第 0 列(row 0) | 第 1 列 ... ... |
+------+------+------+------+------+------+-----+------+
低位址 ------------------------------------------> 高位址
因此 m[i][j] 的位址 = 起點 + (i * 4 + j) * sizeof(int)。
這也解釋了為什麼按列走訪比按欄走訪快(連續記憶體對快取友善)。
初始化可以用巢狀大括號,較易讀:
int m[3][4] = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 },
};
2. 二維陣列傳入函式
編譯器要計算 m[i][j] 的位址,必須知道每列有幾欄;列數反而可以省略:
void print_matrix(int m[][4], int rows); /* 欄數 4 不可省略 */
void print_matrix(int m[3][4], int rows); /* 3 會被編譯器忽略 */
void print_matrix(int (*m)[4], int rows); /* 與上面完全等價 */
int (*m)[4] 讀作「m 是指向『4 個 int 的陣列』的指標」。
注意 int *m[4](指標陣列)與 int (*m)[4](陣列指標)完全不同——括號決定一切。
3. 指標陣列(Array of Pointers)
處理「多個長度不一的字串」時,指標陣列遠比二維字元陣列合適:
char *lines[] = { "hello", "hi", "greetings" };
lines[0] --+--> "hello\0"
lines[1] --+--> "hi\0"
lines[2] --+--> "greetings\0"
(指標本身連續排列;字串散落各處、長度不一)
好處: - 每個字串只占實際需要的空間 - 交換兩列只需交換兩個指標,不必搬動字串本身(K&R 排序文字行的核心技巧)
4. 指標的指標(char **)
指標陣列傳入函式時退化為「指向指標的指標」:
void print_all(char **lines, int n) /* 等同 char *lines[] */
{
for (int i = 0; i < n; i++)
printf("%s\n", lines[i]); /* lines[i] 是第 i 個 char * */
}
argv 就是最著名的例子:int main(int argc, char *argv[]),
其中 argv 是「指向字串指標陣列」的 char **。
5. 指標陣列 vs 二維陣列
char a[10][20] |
char *a[10] |
|
|---|---|---|
| 記憶體 | 固定 200 bytes,連續 | 10 個指標 + 各字串實際長度 |
| 每列長度 | 一律 20(浪費或不足) | 可各自不同 |
| 交換兩列 | 要搬 20 bytes | 只換兩個指標 |
| 初始化後 | 內容可修改 | 指向字面值時內容唯讀 |
| a[i] 是什麼 | 第 i 列的(固定位置)位址 | 第 i 個指標變數的值 |
6. K&R 月份名稱範例(§5.8)
指標陣列初始化的經典示範——用非法月份的守衛值處理錯誤輸入:
char *month_name(int n)
{
static char *name[] = {
"Illegal month",
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
};
return (n < 1 || n > 12) ? name[0] : name[n];
}
static 讓陣列在函式返回後仍然存在,回傳其中的指標才安全。
範例程式
| 檔案 | 內容 |
|---|---|
01_matrix_layout.c |
二維陣列宣告/初始化/走訪、row-major 佈局驗證、傳入函式(含 int (*)[N] 寫法)、矩陣轉置 |
02_month_name.c |
K&R 月份名稱範例 + day_of_year/month_day 日期轉換(§5.7 的閏年表) |
03_pointer_array_vs_2d.c |
指標陣列 vs 二維字元陣列的對比、交換指標排序字串、char ** 走訪 |
編譯與執行(以第一個為例):
cc -std=c11 -Wall -Wextra -o /tmp/demo 01_matrix_layout.c
/tmp/demo
常見錯誤與陷阱
1. 函式參數省略欄數
void f(int m[][], int r, int c); /* 編譯錯誤:欄數必須給 */
修正:void f(int m[][4], int r);。若欄數要能變動,
可用 C99 的變長陣列參數 void f(int r, int c, int m[r][c]); 或改傳一維陣列自己算索引。
2. 把 int ** 當成二維陣列傳(或反過來)
void f(int **p);
int m[3][4];
f(m); /* 錯!m 退化成 int (*)[4],不是 int ** */
二維陣列記憶體中沒有指標表,int ** 期望的是「指標的陣列」。兩者不可混用。
3. 修改指標陣列所指的字串字面值
char *names[] = { "alice", "bob" };
names[0][0] = 'A'; /* 未定義行為:字面值唯讀 */
修正:宣告為 const char *names[] 讓編譯器擋下,或改用二維陣列存放可修改副本。
4. 以為 m[1] 是指標變數
int m[3][4] 中,m[1] 是「第 1 列陣列」退化成的位址值,不是可指派的變數;
m[1] = m[2]; 不合法。指標陣列的 a[1] = a[2]; 則完全合法。
練習題
- 寫
void identity(int m[][4], int n):把方陣設為單位矩陣,印出驗證。 - 寫
int trace(int m[][4], int n):回傳方陣對角線元素之和。 - 寫
char *weekday_name(int d):仿照month_name,回傳星期名稱(含非法輸入守衛)。 - 寫
void sort_strings(const char *a[], int n): 用strcmp比較、只交換指標,把字串陣列按字典序排序。 - 實作 K&R §5.7 的
day_of_year的反函式month_day(int year, int yearday, int *pmonth, int *pday), 用指標參數回傳月與日。 - (進階)寫一個矩陣乘法
void matmul(const int a[][3], const int b[][3], int out[][3]), 3×3 矩陣相乘並印出結果。
延伸閱讀
- K&R 2nd Edition,§5.6–5.9(本單元主軸)
- K&R §5.6 的排序程式:指標陣列在 qsort 文字行的完整應用
- comp.lang.c FAQ 6.18–6.20:二維陣列傳遞的各種疑難
- 下一單元
04-strings-advanced:string.h 完整導覽與字串安全