本檔收錄 5 題由易到難的練習。建議流程:先蓋住解答自己寫,卡住再看 「解題思路」,寫完對照「參考解答」。每題的完整程式碼放在 exercises/:

cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_read_decls.c && /tmp/out

本單元的核心工具是右左法則:從識別字出發,先往右讀(陣列 []、 函式 () 優先權高),碰到 ) 折返往左讀(* 是指標),一層層剝開。


練習 1:讀懂三條經典宣告(難度:基礎)

題目

不查工具,寫出以下宣告的中文描述,然後寫一支程式實際使用這三條宣告, 以行為驗證你的理解:

const char **argv;
double (*m)[3];
void (*sig)(int);

驗證要求:argv 走訪一個字串陣列;m 指向 double grid[2][3] 並證明 m+1 前進「一整列」(24 bytes);sig 指向函式並成功呼叫。

解題思路

一條一條用右左法則推:

  1. const char **argv - 從 argv 出發:右邊沒東西,往左是 *——argv 是指標; - 再往左又是 *——指向「指標」; - 最後 const char——最內層指到 const char。 - 全句:argv 是「指向『指向 const char 的指標』的指標」。 - 這正是字串陣列的存取型別:argv[i] 是一個 const char *。
  2. double (*m)[3] - m 被括號包住,先讀括號內:*m——m 是指標; - 出括號往右:[3]——指向「3 元素陣列」; - 往左:元素是 double。 - 全句:m 是「指向 3 個 double 的陣列」的指標。 - 關鍵推論:m+1 要跨過一整個 double[3],也就是 24 bytes—— 這是它與 double *m(+1 只走 8 bytes)的本質差異。
  3. void (*sig)(int) - 括號內 *sig——sig 是指標; - 出括號往右 (int)——指向「吃一個 int 的函式」; - 往左 void——函式回傳 void。 - 全句:sig 是「指向『吃 int、回傳 void 的函式』的指標」。 - 這是 signal() handler 的型別。
  4. 驗證策略:宣告對不對,行為說了算——m[1][2] 應該拿到 第 1 列第 2 行;(char*)(m+1) - (char*)m 應該是 24。

參考解答

/*
 * ex1_read_decls.c — 練習 1:讀懂三條經典宣告(以可執行程式驗證)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex1_read_decls.c
 * 執行:/tmp/out
 *
 * 三條宣告的中文描述(右左法則推導見 EXERCISES.md):
 *   const char **argv;   argv 是「指向『指向 const char 的指標』的指標」
 *   double (*m)[3];      m 是「指向『3 個 double 的陣列』的指標」
 *   void (*sig)(int);    sig 是「指向『吃 int、回傳 void 的函式』的指標」
 * 本程式對每條宣告實際賦值、使用,證明描述正確。
 */
#include <stdio.h>

static void on_signal(int signo)
{
    printf("    on_signal called with signo=%d\n", signo);
}

int main(void)
{
    printf("=== 1. const char **argv ===\n");
    /*
     * 「指向(指向 const char 的指標)的指標」:
     * 典型用途就是字串陣列——argv[i] 是 const char*,argv 指向第一個。
     */
    const char *words[] = { "hello", "world", NULL };
    const char **argv = words;              /* 指向陣列第一個元素 */
    for (const char **p = argv; *p != NULL; p++)
        printf("  *p = \"%s\"\n", *p);
    /* (*argv)[0] = 'H';  <- 編譯錯誤:最內層是 const char,不可改 */

    printf("\n=== 2. double (*m)[3] ===\n");
    /*
     * 「指向 3 個 double 陣列的指標」=二維陣列退化後的型別。
     * grid[2][3] 傳進函式時就退化成 double (*)[3]。
     */
    double grid[2][3] = { { 1.0, 2.0, 3.0 }, { 4.0, 5.0, 6.0 } };
    double (*m)[3] = grid;                  /* m 指向第 0 列(一整列!) */
    printf("  m[1][2] = %.1f\n", m[1][2]);  /* 語法與二維陣列相同 */
    /* m+1 前進「一整列」= 3 個 double = 24 bytes,不是 8 */
    printf("  (char*)(m+1) - (char*)m = %td bytes (one whole row)\n",
           (char *)(m + 1) - (char *)m);

    printf("\n=== 3. void (*sig)(int) ===\n");
    /*
     * 「指向(吃 int 回傳 void 的函式)的指標」:
     * 這正是 signal() 的 handler 參數型別。
     */
    void (*sig)(int) = on_signal;
    printf("  calling through sig:\n");
    sig(42);

    return 0;
}

程式碼講解

  • const char **p = argv; *p != NULL; p++:以 NULL 當哨兵走訪 字串陣列,與真正的 argv(argv[argc] == NULL)同一套路。
  • 被註解掉的 (*argv)[0] = 'H'; 是刻意留下的「反證」: 打開它編譯器立刻報錯,證明 const 的位置管到最內層。
  • m[1][2] 能用二維語法,是因為 m[1] 先解出「第 1 列」 (型別 double[3]),再 [2] 取元素——與 grid[1][2] 完全等價。
  • 易錯處:把 double (*m)[3] 寫成 double *m[3]——後者是 「3 個 double 指標的陣列」,兩者天差地遠;括號決定一切。

範例輸出

=== 1. const char **argv ===
  *p = "hello"
  *p = "world"

=== 2. double (*m)[3] ===
  m[1][2] = 6.0
  (char*)(m+1) - (char*)m = 24 bytes (one whole row)

=== 3. void (*sig)(int) ===
  calling through sig:
    on_signal called with signo=42

練習 2:指標陣列與 const 的位置(難度:基礎)

題目

宣告一個「指向 const double 的指標的 4 元素陣列」,初始化其中兩個元素 (指向 pi 與 e),其餘為 NULL。印出每個元素指到的值, 並用程式與註解說明:哪些操作合法(改指標本身)、哪些不合法(改指到的值)。

解題思路

  1. 先把中文翻成宣告:目標是「陣列 → 元素是指標 → 指向 const double」。由內而外寫:元素型別是 const double *, 所以陣列宣告是 const double *arr[4];。
  2. 用右左法則反向驗證:arr → [4] 陣列 → * 指標 → const double。與題目要求一致。
  3. 部分初始化:{ &pi, &e } 只給兩個值,C 規定其餘元素 自動補零值(對指標就是 NULL)——這讓「印出 NULL」的分支可驗證。
  4. const 管到哪裡:const 出現在 * 左邊,管的是「指到的東西」: - *arr[0] = 1.0; 不合法(改 const double); - arr[0] = &e; 合法(指標本身不是 const)。 若要鎖住指標本身,const 要放 * 右邊:double *const brr[4]。
  5. 邊界情況:走訪時先檢查 NULL 再解參考,否則未定義行為。

參考解答

/*
 * ex2_const_ptr_array.c — 練習 2:指向 const double 的指標的 4 元素陣列
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex2_const_ptr_array.c
 * 執行:/tmp/out
 *
 * 目標宣告:const double *arr[4];
 * 右左法則:arr -> [4](陣列)-> *(指標)-> const double。
 * 「arr 是 4 元素陣列,每個元素是指向 const double 的指標」。
 */
#include <stdio.h>

int main(void)
{
    static const double pi = 3.141592653589793;
    static const double e  = 2.718281828459045;

    /* 目標宣告:初始化其中兩個元素,其餘為 NULL */
    const double *arr[4] = { &pi, &e };     /* [2]、[3] 自動補 NULL */

    printf("const double *arr[4]  --  array[4] of pointer to const double\n\n");
    for (int i = 0; i < 4; i++) {
        if (arr[i] != NULL)
            printf("  arr[%d] -> %.6f\n", i, *arr[i]);
        else
            printf("  arr[%d] -> NULL\n", i);
    }

    /*
     * 什麼可以改、什麼不能改?
     *   *arr[0] = 1.0;   <- 錯:指到的 double 是 const
     *   arr[0]  = &e;    <- 可以:指標本身不是 const
     */
    arr[0] = &e;                            /* 改指標本身:合法 */
    printf("\nafter arr[0] = &e: arr[0] -> %.6f\n", *arr[0]);

    /* 對照組:double *const brr[?] 才是「const 指標」的陣列 */
    printf("\ncompare:\n");
    printf("  const double *a[4]  -- element points to const double\n");
    printf("  double *const b[4]  -- element itself is a const pointer\n");
    return 0;
}

程式碼講解

  • pi、e 宣告成 static const double:取位址的對象必須有 可持續存在的儲存期,且既然指標型別承諾指向 const, 來源本身宣告 const 最誠實。
  • 部分初始化 { &pi, &e }:C 標準保證剩餘元素零初始化, 這是「哨兵掃描」能安全運作的前提。
  • arr[0] = &e; 與(註解中的)*arr[0] = 1.0; 是本題的靈魂對照: const 在 * 左邊鎖資料、在 * 右邊鎖指標。
  • 易錯處:const double *arr[4] 與 double *const arr[4] 與 const double *const arr[4](雙鎖)三者常被混用; 拿捏不準時回到右左法則逐字讀。

範例輸出

const double *arr[4]  --  array[4] of pointer to const double

  arr[0] -> 3.141593
  arr[1] -> 2.718282
  arr[2] -> NULL
  arr[3] -> NULL

after arr[0] = &e: arr[0] -> 2.718282

compare:
  const double *a[4]  -- element points to const double
  double *const b[4]  -- element itself is a const pointer

練習 3:以 int (*grid)[5] 接收二維陣列(難度:進階)

題目

寫一個函式 void print_grid(int (*grid)[5], int rows), 印出二維表格;在 main 以 int g[3][5](填入 r*10+c)呼叫並驗證。 另外用指標算術證明:grid+1 前進的距離是「一整列」(20 bytes)。

解題思路

  1. 退化規則:陣列傳進函式時退化成「指向第一個元素的指標」。 int g[3][5] 的元素是「一整列」int[5],所以退化結果是 int (*)[5]——最外層維度消失,內層維度全保留。
  2. 為什麼列數要另外傳:型別裡只剩 [5](行數), 列數 3 已經不在型別資訊裡,函式無從得知,必須當參數傳。
  3. 為什麼行數不能省:grid[r][c] 的位址計算是 base + (r*5 + c)*sizeof(int)——那個 5 來自型別。 寫成 int grid[][] 編譯器算不出跨列距離,直接拒絕編譯。
  4. 等價寫法:int grid[][5] 與 int (*grid)[5] 在參數位置 完全等價(前者只是語法糖),但在一般變數宣告時兩者不同。
  5. 驗證設計:填 r*10+c 讓「行列錯位」一眼可見 (若把 5 寫成 4,輸出會歪掉);再印 grid+1 - grid 的位元組差, 應為 5 * sizeof(int) = 20。

記憶體圖:

g:  ┌────────────── row 0 ──────────────┬────────────── row 1 ─────...
    │ g[0][0] g[0][1] g[0][2] g[0][3] g[0][4] │ g[1][0] g[1][1] ...
    └────────────────────────────────────┴──────────────────────...
    ^                                    ^
    grid                                 grid+1(跳 20 bytes)

參考解答

/*
 * ex3_grid_param.c — 練習 3:以 int (*grid)[5] 接收二維陣列
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex3_grid_param.c
 * 執行:/tmp/out
 *
 * 二維陣列 int g[3][5] 傳進函式時退化成「指向 5 元素列的指標」
 * int (*grid)[5]——列數退化消失(要另外傳),行數 5 是型別的一部分。
 */
#include <stdio.h>

/*
 * grid 的型別是 int (*)[5]:指向「5 個 int 的陣列」的指標。
 * grid[r] 是第 r 列(型別 int[5]),grid[r][c] 是元素。
 * 也可寫成等價的 void print_grid(int rows, int grid[][5])。
 */
static void print_grid(int (*grid)[5], int rows)
{
    for (int r = 0; r < rows; r++) {
        printf("  row %d:", r);
        for (int c = 0; c < 5; c++)
            printf(" %3d", grid[r][c]);
        printf("\n");
    }
}

/* 順便證明:grid+1 前進一整列(5 個 int = 20 bytes) */
static void show_row_stride(int (*grid)[5])
{
    printf("  grid   = %p\n", (void *)grid);
    printf("  grid+1 = %p  (stride = %td bytes = one row)\n",
           (void *)(grid + 1), (char *)(grid + 1) - (char *)grid);
}

int main(void)
{
    int g[3][5];

    /* 填入 r*10 + c,一眼可驗證行列沒有錯位 */
    for (int r = 0; r < 3; r++)
        for (int c = 0; c < 5; c++)
            g[r][c] = r * 10 + c;

    printf("int g[3][5] passed as int (*grid)[5]:\n");
    print_grid(g, 3);          /* g 自動退化成 &g[0],型別 int(*)[5] */

    printf("\npointer arithmetic on int (*)[5]:\n");
    show_row_stride(g);
    return 0;
}

程式碼講解

  • print_grid(g, 3):g 在此自動退化為 &g[0],型別恰是 int (*)[5],完全不需要 cast。
  • grid[r][c] 兩層下標的展開:grid[r] 先做「指標 + r」 (每步跨一列),解出 int[5];再 [c] 取元素。 所有位移都由型別自動算好——這就是保留內層維度的意義。
  • (char *)(grid + 1) - (char *)grid:想量「位元組距離」就先轉 char *,否則指標相減的單位是「元素個數」。
  • 易錯處:參數寫成 int **grid 是錯的——int[3][5] 是連續的 一塊記憶體,不是「指標的陣列」;int ** 對它做兩層解參考 會把資料當位址使用,多半直接當機。

範例輸出

int g[3][5] passed as int (*grid)[5]:
  row 0:   0   1   2   3   4
  row 1:  10  11  12  13  14
  row 2:  20  21  22  23  24

pointer arithmetic on int (*)[5]:
  grid   = 0x16d04d60c
  grid+1 = 0x16d04d620  (stride = 20 bytes = one row)

(指標位址每次執行不同,stride 恆為 20 bytes。)


練習 4:用 typedef 拆解 handlers 宣告(難度:進階)

題目

把 void (*handlers[10])(int, void *); 用 typedef 拆成兩層具名型別, 寫出等價宣告,並以程式證明兩種寫法型別完全相容 (互相指派、sizeof 相同、都能呼叫)。

解題思路

  1. 先讀懂原始宣告:右左法則從 handlers 出發—— [10] 陣列 → * 指標 → (int, void*) 函式 → void 回傳。 「10 元素陣列,元素是指向『吃 (int, void*)、回傳 void 的函式』 的指標」。
  2. 拆解策略:由內而外。最內層的複雜度來自「函式指標」, 先把它具名:
typedef void (*handler_fn)(int, void *);

這一層拆掉後,原宣告塌縮成人人可讀的 handler_fn handlers[10];。 3. 第二層:若「10 個 handler 的表」本身是常用概念,再包一層:

typedef handler_fn handler_table[10];   /* 陣列 typedef */
handler_table handlers;                 /* 與原宣告等價 */
  1. 驗證相容性:宣告一個「原始寫法」的全域 void (*handlers_raw[10])(int, void *);, 把 typedef 版的元素指派給它並呼叫——編譯器不報錯即證明同型別; 再比較兩者 sizeof(都是 10 個函式指標)。
  2. 回呼帶 context:void * 參數是回呼介面的標準配件, 讓呼叫端傳自己的狀態進 handler(示範傳一個字串)。

參考解答

/*
 * ex4_typedef_layers.c — 練習 4:用 typedef 拆解 handlers 宣告
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex4_typedef_layers.c
 * 執行:/tmp/out
 *
 * 原始宣告:void (*handlers[10])(int, void *);
 * 讀法:handlers -> [10](陣列)-> *(指標)-> (int, void*)(函式)-> void。
 * 「handlers 是 10 元素陣列,元素是指向『吃 (int, void*)、回傳 void
 *   的函式』的指標」。
 * 本程式用兩層 typedef 拆解它,並證明兩種寫法型別相容。
 */
#include <stdio.h>

/* ---- 第一層:把「函式指標」具名 ---- */
typedef void (*handler_fn)(int, void *);

/* ---- 第二層:把「10 個 handler 的表」具名 ---- */
typedef handler_fn handler_table[10];

/* ---- 等價的原始宣告(不用 typedef)---- */
static void (*handlers_raw[10])(int, void *);

/* ---- 兩個示範 handler ---- */
static void on_open(int id, void *ctx)
{
    printf("  on_open : id=%d ctx=\"%s\"\n", id, (const char *)ctx);
}

static void on_close(int id, void *ctx)
{
    printf("  on_close: id=%d ctx=\"%s\"\n", id, (const char *)ctx);
}

int main(void)
{
    /* 用具名型別宣告:意圖一目瞭然 */
    handler_table handlers = { on_open, on_close };  /* 其餘 8 格 = NULL */

    char ctx[] = "session-42";

    printf("dispatch through handler_table:\n");
    for (int i = 0; i < 10; i++)
        if (handlers[i] != NULL)
            handlers[i](i, ctx);

    /*
     * 證明 typedef 版與原始宣告型別完全相同:
     * 兩者的元素可以互相指派,指標型別也一致。
     */
    handlers_raw[0] = handlers[1];
    printf("\nraw declaration is the same type:\n");
    handlers_raw[0](99, ctx);

    /* sizeof 也一致:10 個函式指標 */
    printf("\nsizeof(handler_table)=%zu, sizeof(handlers_raw)=%zu\n",
           sizeof(handler_table), sizeof handlers_raw);
    return 0;
}

程式碼講解

  • 兩層 typedef 各司其職:handler_fn 消滅「函式指標括號」, handler_table 讓「表」成為可傳遞的概念。實務上第一層幾乎必做, 第二層視 API 需要。
  • 陣列 typedef 的小陷阱:handler_table t; 宣告的是陣列, 當函式參數時仍會退化成 handler_fn *——typedef 不改變退化規則。
  • handlers_raw[0] = handlers[1]; 若兩者型別不同,這行無法通過 -Wall -Wextra(不相容指標警告視同失敗),能編譯即證明等價。
  • 易錯處:typedef void (*handler_fn)(int, void *) 忘了括號會變成 「typedef 一個回傳 void * 的函式型別」,錯誤訊息會很難懂; 函式指標 typedef 的括號位置與變數宣告完全相同。

範例輸出

dispatch through handler_table:
  on_open : id=0 ctx="session-42"
  on_close: id=1 ctx="session-42"

raw declaration is the same type:
  on_close: id=99 ctx="session-42"

sizeof(handler_table)=80, sizeof(handlers_raw)=80

練習 5:擴充 mini-dcl 支援參數列(難度:挑戰)

題目

擴充 03_mini_dcl.c 的遞迴下降解析器,支援函式參數列,例如把 int (*f)(int, char *) 翻譯成:

f: pointer to function (int, pointer to char) returning int

要求至少支援:多參數(逗號分隔)、參數的星號(多層指標)、 const/unsigned 等修飾字、可省略的參數名稱、(void)。 巢狀函式指標參數可不支援(註明限制即可)。

解題思路

  1. 先擴文法。原文法的函式只有 direct-dcl ( ),加一條產生式:
direct-dcl: ... | direct-dcl ( params )
params:     param { , param }
param:      type-words { * } [ name ]
  1. 關鍵歧義:( 是分組還是參數列? 原程式在 gettoken 就把 () 特判成 PARENS;剩下的 ( 有兩種可能。觀察文法可發現: - 分組括號只會出現在 direct-dcl 的開頭(( dcl )); - 出現在後綴位置(已解析完 name 或 (dcl) 之後)的 ( 必定是參數列。 所以不用改 lexer,只要在 dirdcl 的後綴迴圈多接一個 '(' 分支。
  2. 參數怎麼描述:一個參數 = 型別字序列 + 星號 + 可選名稱。 - 型別字 vs 參數名稱怎麼分?C 編譯器靠符號表;我們用簡化規則: 維護一張關鍵字表(void、char、const、unsigned…), 在表內的是型別字,不在表內的識別字視為參數名稱(忽略)。 - 每顆星翻譯成一層 pointer to,接上型別字, char * → pointer to char,const void * → pointer to const void。
  3. 組裝輸出:把所有參數描述用 ", " 接起來,包進 " function (…) returning",接到既有的輸出流裡—— 遞迴結構完全不動,這是遞迴下降解析器「加產生式不動骨架」的優點。
  4. 邊界情況:(void) 是「無參數」的正式寫法,關鍵字表含 void 所以自然處理;空參數列 () 走原本的 PARENS 分支 (輸出不含參數清單,維持與 K&R 語意一致:參數未指定)。
  5. 測試設計:樣本涵蓋——頂層函式、函式指標、陣列元素是函式指標、 qsort 比較函式(雙 const)、回傳函式指標的函式、 以及舊文法的 K&R 經典宣告(迴歸測試)。

參考解答

/*
 * ex5_dcl_params.c — 練習 5:擴充 mini-dcl,支援解析參數列
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex5_dcl_params.c
 * 執行:/tmp/out
 *
 * 在 03_mini_dcl.c 的文法上新增 params 規則:
 *
 *   dcl:        { * } direct-dcl
 *   direct-dcl: name
 *               | ( dcl )
 *               | direct-dcl ( )            (函式,參數未指定)
 *               | direct-dcl ( params )     (函式,含參數列)★新增
 *               | direct-dcl [ size ]       (陣列)
 *   params:     param { , param }
 *   param:      type-words { * } [ name ]   (簡化:不支援巢狀函式指標參數)
 *
 * 例:int (*f)(int, char *)
 *  -> f: pointer to function (int, pointer to char) returning int
 */
#include <stdio.h>
#include <string.h>
#include <ctype.h>

#define MAXTOKEN  64
#define MAXOUT    512

enum { NAME, PARENS, BRACKETS, TOKEN_EOF, TOKEN_CHAR };

static const char *src;            /* 目前掃描位置 */
static char  token[MAXTOKEN];      /* 最新 token 的文字 */
static int   tokentype;            /* 最新 token 的種類 */
static char  name[MAXTOKEN];       /* 識別字名稱 */
static char  out[MAXOUT];          /* 累積的英文輸出 */
static int   had_error;

/* 型別/修飾字關鍵字表:用來區分「型別字」與「參數名稱」 */
static int is_type_word(const char *s)
{
    static const char *kw[] = {
        "void", "char", "short", "int", "long", "float", "double",
        "signed", "unsigned", "const", "volatile", "struct", "union",
        "enum", "_Bool",
    };
    for (size_t i = 0; i < sizeof kw / sizeof kw[0]; i++)
        if (strcmp(s, kw[i]) == 0)
            return 1;
    return 0;
}

static int gettoken(void)
{
    while (*src == ' ' || *src == '\t')
        src++;

    if (*src == '\0') {
        tokentype = TOKEN_EOF;
        return tokentype;
    }
    if (*src == '(') {
        src++;
        while (*src == ' ') src++;
        if (*src == ')') {                 /* 空參數列 () -> 函式 */
            src++;
            strcpy(token, "()");
            tokentype = PARENS;
        } else {
            tokentype = '(';               /* 分組括號「或」參數列開頭 */
        }
        return tokentype;
    }
    if (*src == '[') {                     /* 收集整個 [size] */
        char *p = token;
        *p++ = *src++;
        while (*src != '\0' && *src != ']')
            *p++ = *src++;
        if (*src == ']')
            *p++ = *src++;
        *p = '\0';
        tokentype = BRACKETS;
        return tokentype;
    }
    if (isalpha((unsigned char)*src) || *src == '_') {
        char *p = token;
        while (isalnum((unsigned char)*src) || *src == '_')
            *p++ = *src++;
        *p = '\0';
        tokentype = NAME;
        return tokentype;
    }
    tokentype = (unsigned char)*src++;     /* 其他單一字元:* , ) 等 */
    return tokentype;
}

static void out_append(const char *s)
{
    if (strlen(out) + strlen(s) < MAXOUT - 1)
        strcat(out, s);
}

static void dcl(void);

/*
 * ★新增:解析單一參數「type-words {*} [name]」,
 * 把英文描述(如 "pointer to const char")寫進 buf。
 */
static void parse_one_param(char *buf, size_t bufsz)
{
    char typewords[MAXTOKEN * 2] = "";
    int  nstars = 0;

    /* 1. 收集連續的型別字(const char、unsigned int...) */
    while (tokentype == NAME && is_type_word(token)) {
        if (typewords[0] != '\0')
            strncat(typewords, " ", sizeof typewords - strlen(typewords) - 1);
        strncat(typewords, token, sizeof typewords - strlen(typewords) - 1);
        gettoken();
    }
    if (typewords[0] == '\0') {            /* 沒看到型別字:語法錯誤 */
        had_error = 1;
        return;
    }

    /* 2. 收集星號 */
    while (tokentype == '*') {
        nstars++;
        gettoken();
    }

    /* 3. 選擇性的參數名稱(非關鍵字的識別字),描述時忽略它 */
    if (tokentype == NAME && !is_type_word(token))
        gettoken();

    /* 4. 組出英文:每顆星是一層 "pointer to " */
    buf[0] = '\0';
    for (int i = 0; i < nstars; i++)
        strncat(buf, "pointer to ", bufsz - strlen(buf) - 1);
    strncat(buf, typewords, bufsz - strlen(buf) - 1);
}

/*
 * ★新增:解析參數列。呼叫時剛讀到 '('(且已知不是 "()")。
 * 產出 " function (int, pointer to char) returning"。
 */
static void parse_params(void)
{
    char plist[MAXOUT] = "";
    char one[MAXTOKEN * 4];

    gettoken();                            /* 進到第一個參數的開頭 */
    for (;;) {
        parse_one_param(one, sizeof one);
        if (had_error)
            return;
        if (plist[0] != '\0')
            strncat(plist, ", ", sizeof plist - strlen(plist) - 1);
        strncat(plist, one, sizeof plist - strlen(plist) - 1);

        if (tokentype == ',') {            /* 還有下一個參數 */
            gettoken();
            continue;
        }
        break;
    }
    if (tokentype != ')') {                /* 參數列必須以 ) 收尾 */
        had_error = 1;
        return;
    }

    out_append(" function (");
    out_append(plist);
    out_append(") returning");
}

static void dirdcl(void)
{
    int type;

    if (tokentype == '(') {          /* 形式:( dcl ) */
        dcl();
        if (tokentype != ')') {
            had_error = 1;
            return;
        }
    } else if (tokentype == NAME) {  /* 形式:identifier */
        strcpy(name, token);
    } else {
        had_error = 1;
        return;
    }

    /*
     * 向右收集後綴。注意 '(' 出現在「後綴位置」時必為參數列
     * (分組括號只可能出現在 dirdcl 的開頭),因此可以放心解析。
     */
    while ((type = gettoken()) == PARENS || type == BRACKETS
           || type == '(') {
        if (type == PARENS) {
            out_append(" function returning");
        } else if (type == '(') {
            parse_params();                /* ★新增的分支 */
            if (had_error)
                return;
        } else {
            out_append(" array");
            if (token[1] != ']') {
                char sizebuf[MAXTOKEN];
                size_t len = strlen(token) - 2;
                out_append(" ");
                memcpy(sizebuf, token + 1, len);
                sizebuf[len] = '\0';
                out_append(sizebuf);
            }
            out_append(" of");
        }
    }
}

static void dcl(void)
{
    int ns = 0;

    while (gettoken() == '*')
        ns++;
    dirdcl();
    while (ns-- > 0)
        out_append(" pointer to");
}

static void explain(const char *decl)
{
    char type[MAXTOKEN];

    src = decl;
    out[0] = '\0';
    name[0] = '\0';
    had_error = 0;

    gettoken();
    if (tokentype != NAME) {
        printf("  %-36s -> syntax error (no base type)\n", decl);
        return;
    }
    strcpy(type, token);

    dcl();

    if (had_error || tokentype != TOKEN_EOF)
        printf("  %-36s -> syntax error\n", decl);
    else
        printf("  %-36s -> %s:%s %s\n", decl, name, out, type);
}

int main(void)
{
    const char *samples[] = {
        "int f(int, char *)",
        "int (*f)(int, char *)",
        "void (*sig)(int)",
        "void (*handlers[10])(int, void *)",
        "int (*cmp)(const void *a, const void *b)",
        "double (*g(void))(unsigned long, float *)",
        "char (*(*x[3])())[5]",              /* 舊文法(空參數列)仍可用 */
    };
    size_t n = sizeof samples / sizeof samples[0];

    printf("=== mini-dcl with parameter lists ===\n\n");
    for (size_t i = 0; i < n; i++)
        explain(samples[i]);

    printf("\nlimitation: nested function-pointer parameters are not\n");
    printf("supported (a real cdecl parses param lists recursively).\n");
    return 0;
}

程式碼講解

  • is_type_word 是簡化版的「符號表」:真正的編譯器要處理 typedef 名稱(這也是 C 文法出名難解析的原因——a * b; 是乘法還是宣告, 取決於 a 是不是型別)。教學版用固定關鍵字表換取簡潔。
  • parse_one_param 的步驟順序(型別字 → 星號 → 名稱)直接對映 參數文法;描述時把參數名稱讀掉但丟棄,因為型別描述不需要它。
  • parse_params 用「先收集到 plist、最後一次輸出」的策略, 而不是邊解析邊 out_append——這樣錯誤時不會留下半截輸出。
  • 後綴位置的 '(' 必為參數列,這個推理是本題的核心觀察; 想不通時回去看文法:( dcl ) 只出現在 direct-dcl 開頭。
  • 易錯處:解析完最後一個參數後,目前 token 應該正好是 ')'; 忘了檢查會讓 int f(int x 這種殘缺輸入矇混過關。
  • 已知限制:參數本身是函式指標(如 int (*g)(void) 當參數)需要 遞迴呼叫 dcl,牽動全域狀態的儲存與還原,留給有興趣的讀者。

範例輸出

=== mini-dcl with parameter lists ===

  int f(int, char *)                   -> f: function (int, pointer to char) returning int
  int (*f)(int, char *)                -> f: pointer to function (int, pointer to char) returning int
  void (*sig)(int)                     -> sig: pointer to function (int) returning void
  void (*handlers[10])(int, void *)    -> handlers: array 10 of pointer to function (int, pointer to void) returning void
  int (*cmp)(const void *a, const void *b) -> cmp: pointer to function (pointer to const void, pointer to const void) returning int
  double (*g(void))(unsigned long, float *) -> g: function (void) returning pointer to function (unsigned long, pointer to float) returning double
  char (*(*x[3])())[5]                 -> x: array 3 of pointer to function returning pointer to array 5 of char

limitation: nested function-pointer parameters are not
supported (a real cdecl parses param lists recursively).

常見錯誤解法對照

錯誤寫法 症狀 正解
double *m[3] 想接二維陣列 型別不相容警告/當機 double (*m)[3](練習 1、3)
int **grid 接 int[3][5] 把資料當位址解參考,當機 int (*grid)[5](練習 3)
const 位置搞錯想鎖指標 該擋的沒擋住 * 左鎖資料、右鎖指標(練習 2)
typedef 函式指標忘記括號 變成回傳指標的函式型別 typedef void (*fn)(int, void*)(練習 4)
分組括號與參數列不分 解析器把 (int,...) 當巢狀宣告 後綴位置的 ( 必為參數列(練習 5)