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)

解讀步驟:

  1. 找到識別字(變數名)。
  2. 從識別字向右讀:遇到 [N] 唸「…的陣列」,遇到 (...) 唸「回傳…的函式」,直到右括號或結尾。
  3. 再向左讀:遇到 * 唸「指向…的指標」,遇到型別唸出型別,直到左括號。
  4. 跳出一層括號,重複 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

常見錯誤與陷阱

  1. int* a, b; 只有 a 是指標:* 屬於宣告子不屬於型別,b 是普通 int。 一行一個宣告、或把 * 貼著變數名寫,可避免誤會。
  2. 把 const int *p 誤以為指標不可改:其實是「資料」不可透過 p 改, p = other; 完全合法;要鎖住指標本身得寫 int * const p。
  3. 函式參數 int a[] 造成的錯覺:參數中的陣列一律退化為指標, sizeof a 得到的是指標大小而非陣列大小,二維陣列參數必須寫成 int (*)[N] 且 N 不可省。
  4. 對 int (*b)[10] 直接 b[i] 取值:b[0] 是「整個陣列」不是 int, 要寫 (*b)[i] 或 b[0][i]。
  5. 省略括號:int *f(void) 與 int (*f)(void) 天差地遠,前者是函式宣告, 後者才是指標變數。

練習題

  1. 【易】不查工具,寫出以下宣告的中文描述:const char **argv;、double (*m)[3];、 void (*sig)(int);,再用 cdecl 驗證。
  2. 【易】宣告一個「指向 const double 的指標的 4 元素陣列」,並初始化其中兩個元素。
  3. 【中】寫一個函式接收 int (*grid)[5] 與列數,印出二維表格; 在 main 用 int g[3][5] 呼叫並驗證。
  4. 【中】用 typedef 把 void (*handlers[10])(int, void *) 拆成兩層具名型別, 並寫出等價宣告。
  5. 【難】解讀 char *(*(**foo[][8])())[];(提示:從 foo 開始右左法則), 寫出完整英文描述後用 cdecl 驗證。
  6. 【難】擴充 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''"(另一種記憶法,注意其局限)