C 的宣告語法遵循「宣告模仿使用」(declaration mirrors use)的哲學,優雅但也造就了
char (*(*x[3])())[5] 這種令人望之生畏的天書。本單元教你系統化的解讀方法——右左法則
(也稱 spiral rule 的嚴謹版),並徹底釐清 const 與指標、陣列指標與指標陣列、
函式指標的各種組合。對應 K&R §5.12,該節的 dcl 程式正是為了把宣告翻譯成英文而寫的。
學習目標
- 掌握「右左法則」,能徒手解讀任意複雜的 C 宣告
- 分辨
const int *、int * const、const int * const三者差異 - 分辨「陣列指標」
int (*p)[N]與「指標陣列」int *p[N] - 看懂含函式指標的複合宣告,如
int (*(*f)(int))[5] - 理解 K&R
dcl程式(宣告 ↔ 英文描述雙向轉換)的解析思路 - 會使用 cdecl 工具驗證自己的解讀
對照章節 / 參考來源
- K&R §5.12 Complicated Declarations(
dcl與undcl程式) - K&R §2.4 Declarations、§5.1–5.9(指標與陣列基礎)
- C11 標準 §6.7.6 Declarators
- cdecl.org:線上宣告翻譯工具(
brew install cdecl可裝本機版)
觀念講解
1. 右左法則(Right-Left Rule)
解讀步驟:
- 找到識別字(變數名)。
- 從識別字向右讀:遇到
[N]唸「…的陣列」,遇到(...)唸「回傳…的函式」,直到右括號或結尾。 - 再向左讀:遇到
*唸「指向…的指標」,遇到型別唸出型別,直到左括號。 - 跳出一層括號,重複 2–3。
範例逐步拆解 char (*(*x[3])(void))[5]:
x -- x 是
x[3] -- 3 個元素的陣列,元素是
(*x[3]) -- 指標,指向
(*x[3])(void) -- 不接收參數的函式,回傳
(*(*x[3])(void)) -- 指標,指向
char (...)[5] -- 5 個 char 的陣列
完整讀法:「x 是一個 3 元素陣列,每個元素是函式指標,該函式不接收參數, 回傳指向 5 個 char 陣列的指標」。
記憶要訣:右邊的 [] 與 () 優先權高於左邊的 *,括號用來扭轉這個順序。
2. const 與指標的三種組合
const 修飾它左邊最近的東西(若左邊沒有東西才修飾右邊):
const int *p; /* p 指向 const int:資料唯讀,指標可改指別處 */
int const *p; /* 與上一行完全相同(const 在 int 右邊) */
int * const p; /* p 本身是 const:指標不可改,但 *p 可寫 */
const int * const p; /* 兩者皆 const:指標與資料都不可改 */
判讀技巧:以 * 為分界——const 在 * 左邊,保護「被指向的資料」;
const 在 * 右邊,保護「指標本身」。
實務應用:函式參數寫 const char *s 是對呼叫者的承諾(不會改你的字串),
編譯器會擋下函式內部的誤寫,同時允許呼叫端傳入字串常量。
3. 陣列指標 vs 指標陣列
int *a[10]; /* a 是「10 個 int* 的陣列」——指標陣列 */
int (*b)[10]; /* b 是「指向 10 個 int 陣列的指標」——陣列指標 */
ASCII 圖對照:
int *a[10](指標陣列) int (*b)[10](陣列指標)
a: [ptr0][ptr1]...[ptr9] b: [ptr] ──> [i0][i1]...[i9]
│ │ (一個指標,指向一整塊
v v 10 個 int 的陣列)
int int (各自指向不同地方)
陣列指標最重要的用途是二維陣列傳參:
void f(int (*m)[4], int rows); /* 接收 int m[][4] */
int grid[3][4];
f(grid, 3); /* grid 退化為 int(*)[4] */
sizeof 也能揭露差異:sizeof(*b) 是 10 * sizeof(int)(整個陣列),
而 sizeof(*a[0]) 只是 sizeof(int)。
4. 函式指標的複雜組合
int (*f)(int); /* f:指向「int -> int 函式」的指標 */
int (*g[8])(int); /* g:8 個上述函式指標的陣列 */
int (*(*h)[8])(int); /* h:指向上述陣列的指標 */
int (*k(int))(int); /* k:函式,接收 int,回傳函式指標 */
超過兩層就該用 typedef 分層命名:
typedef int (*handler_fn)(int); /* 第一層:函式指標 */
typedef handler_fn handler_tbl[8]; /* 第二層:函式指標陣列 */
handler_tbl *h; /* h 一目了然 */
5. K&R dcl 程式的概念
K&R §5.12 的 dcl 是一個遞迴下降解析器(recursive descent parser),文法為:
dcl: 可選的 * 序列,接 direct-dcl
direct-dcl: name
| ( dcl )
| direct-dcl ( ) -- 函式
| direct-dcl [size] -- 陣列
解析策略正是右左法則的程式化:direct-dcl 先吃識別字(或括號內的子宣告),
再向右收集 ()/[],回溯時才把左邊累積的 * 加上去。輸出時逐層印出
"pointer to"、"array of"、"function returning",就得到英文描述。
本單元的 03_mini_dcl.c 實作了一個簡化版供你研究。
6. cdecl 工具
驗證解讀最快的辦法:
$ cdecl explain 'char (*(*x[3])())[5]'
declare x as array 3 of pointer to function returning pointer to array 5 of char
$ cdecl declare 'p as pointer to function (int) returning pointer to int'
int *(*p)(int )
線上版 cdecl.org 免安裝;macOS 可 brew install cdecl。
養成習慣:寫出複雜宣告後先丟給 cdecl 驗證,再放進程式碼並加註解。
範例程式
| 檔案 | 內容 | 編譯 / 執行 |
|---|---|---|
01_const_pointers.c |
const 三種組合的實驗、函式參數的 const 慣例 | cc -std=c11 -Wall -Wextra -o out 01_const_pointers.c && ./out |
02_arrays_of_pointers.c |
指標陣列 vs 陣列指標、sizeof 差異、二維陣列傳參 | cc -std=c11 -Wall -Wextra -o out 02_arrays_of_pointers.c && ./out |
03_mini_dcl.c |
簡化版 K&R dcl:把寫死的宣告字串翻譯成英文描述 | cc -std=c11 -Wall -Wextra -o out 03_mini_dcl.c && ./out |
常見錯誤與陷阱
int* a, b;只有a是指標:*屬於宣告子不屬於型別,b是普通int。 一行一個宣告、或把*貼著變數名寫,可避免誤會。- 把
const int *p誤以為指標不可改:其實是「資料」不可透過 p 改,p = other;完全合法;要鎖住指標本身得寫int * const p。 - 函式參數
int a[]造成的錯覺:參數中的陣列一律退化為指標,sizeof a得到的是指標大小而非陣列大小,二維陣列參數必須寫成int (*)[N]且 N 不可省。 - 對
int (*b)[10]直接b[i]取值:b[0]是「整個陣列」不是 int, 要寫(*b)[i]或b[0][i]。 - 省略括號:
int *f(void)與int (*f)(void)天差地遠,前者是函式宣告, 後者才是指標變數。
練習題
- 【易】不查工具,寫出以下宣告的中文描述:
const char **argv;、double (*m)[3];、void (*sig)(int);,再用 cdecl 驗證。 - 【易】宣告一個「指向 const double 的指標的 4 元素陣列」,並初始化其中兩個元素。
- 【中】寫一個函式接收
int (*grid)[5]與列數,印出二維表格; 在 main 用int g[3][5]呼叫並驗證。 - 【中】用 typedef 把
void (*handlers[10])(int, void *)拆成兩層具名型別, 並寫出等價宣告。 - 【難】解讀
char *(*(**foo[][8])())[];(提示:從 foo 開始右左法則), 寫出完整英文描述後用 cdecl 驗證。 - 【難】擴充
03_mini_dcl.c:支援解析參數列(如int (*f)(int, char *)), 輸出 "function (int, pointer to char) returning ..."。
延伸閱讀
- K&R §5.12 完整的 dcl/undcl 程式碼
- 《Expert C Programming》第 3 章 "Unscrambling Declarations in C"(右左法則的出處之一)
- cppreference:C declaration syntax
cdecl原始碼(一個成熟的宣告解析器範例)- David Anderson, "The ``Clockwise/Spiral Rule''"(另一種記憶法,注意其局限)