本檔收錄 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 指向函式並成功呼叫。
解題思路
一條一條用右左法則推:
const char **argv- 從argv出發:右邊沒東西,往左是*——argv 是指標; - 再往左又是*——指向「指標」; - 最後const char——最內層指到 const char。 - 全句:argv 是「指向『指向 const char 的指標』的指標」。 - 這正是字串陣列的存取型別:argv[i]是一個const char *。double (*m)[3]-m被括號包住,先讀括號內:*m——m 是指標; - 出括號往右:[3]——指向「3 元素陣列」; - 往左:元素是double。 - 全句:m 是「指向 3 個 double 的陣列」的指標。 - 關鍵推論:m+1要跨過一整個double[3],也就是 24 bytes—— 這是它與double *m(+1 只走 8 bytes)的本質差異。void (*sig)(int)- 括號內*sig——sig 是指標; - 出括號往右(int)——指向「吃一個 int 的函式」; - 往左void——函式回傳 void。 - 全句:sig 是「指向『吃 int、回傳 void 的函式』的指標」。 - 這是signal()handler 的型別。- 驗證策略:宣告對不對,行為說了算——
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。印出每個元素指到的值,
並用程式與註解說明:哪些操作合法(改指標本身)、哪些不合法(改指到的值)。
解題思路
- 先把中文翻成宣告:目標是「陣列 → 元素是指標 → 指向 const
double」。由內而外寫:元素型別是
const double *, 所以陣列宣告是const double *arr[4];。 - 用右左法則反向驗證:
arr→[4]陣列 →*指標 →const double。與題目要求一致。 - 部分初始化:
{ &pi, &e }只給兩個值,C 規定其餘元素 自動補零值(對指標就是 NULL)——這讓「印出 NULL」的分支可驗證。 - const 管到哪裡:
const出現在*左邊,管的是「指到的東西」: -*arr[0] = 1.0;不合法(改 const double); -arr[0] = &e;合法(指標本身不是 const)。 若要鎖住指標本身,const 要放*右邊:double *const brr[4]。 - 邊界情況:走訪時先檢查 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)。
解題思路
- 退化規則:陣列傳進函式時退化成「指向第一個元素的指標」。
int g[3][5]的元素是「一整列」int[5],所以退化結果是int (*)[5]——最外層維度消失,內層維度全保留。 - 為什麼列數要另外傳:型別裡只剩
[5](行數), 列數 3 已經不在型別資訊裡,函式無從得知,必須當參數傳。 - 為什麼行數不能省:
grid[r][c]的位址計算是base + (r*5 + c)*sizeof(int)——那個 5 來自型別。 寫成int grid[][]編譯器算不出跨列距離,直接拒絕編譯。 - 等價寫法:
int grid[][5]與int (*grid)[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 相同、都能呼叫)。
解題思路
- 先讀懂原始宣告:右左法則從
handlers出發——[10]陣列 →*指標 →(int, void*)函式 →void回傳。 「10 元素陣列,元素是指向『吃 (int, void*)、回傳 void 的函式』 的指標」。 - 拆解策略:由內而外。最內層的複雜度來自「函式指標」, 先把它具名:
typedef void (*handler_fn)(int, void *);
這一層拆掉後,原宣告塌縮成人人可讀的 handler_fn handlers[10];。
3. 第二層:若「10 個 handler 的表」本身是常用概念,再包一層:
typedef handler_fn handler_table[10]; /* 陣列 typedef */
handler_table handlers; /* 與原宣告等價 */
- 驗證相容性:宣告一個「原始寫法」的全域
void (*handlers_raw[10])(int, void *);, 把 typedef 版的元素指派給它並呼叫——編譯器不報錯即證明同型別; 再比較兩者sizeof(都是 10 個函式指標)。 - 回呼帶 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)。
巢狀函式指標參數可不支援(註明限制即可)。
解題思路
- 先擴文法。原文法的函式只有
direct-dcl ( ),加一條產生式:
direct-dcl: ... | direct-dcl ( params )
params: param { , param }
param: type-words { * } [ name ]
- 關鍵歧義:
(是分組還是參數列? 原程式在gettoken就把()特判成 PARENS;剩下的(有兩種可能。觀察文法可發現: - 分組括號只會出現在direct-dcl的開頭(( dcl )); - 出現在後綴位置(已解析完 name 或(dcl)之後)的(必定是參數列。 所以不用改 lexer,只要在dirdcl的後綴迴圈多接一個'('分支。 - 參數怎麼描述:一個參數 = 型別字序列 + 星號 + 可選名稱。
- 型別字 vs 參數名稱怎麼分?C 編譯器靠符號表;我們用簡化規則:
維護一張關鍵字表(
void、char、const、unsigned…), 在表內的是型別字,不在表內的識別字視為參數名稱(忽略)。 - 每顆星翻譯成一層pointer to,接上型別字,char *→pointer to char,const void *→pointer to const void。 - 組裝輸出:把所有參數描述用
", "接起來,包進" function (…) returning",接到既有的輸出流裡—— 遞迴結構完全不動,這是遞迴下降解析器「加產生式不動骨架」的優點。 - 邊界情況:
(void)是「無參數」的正式寫法,關鍵字表含void所以自然處理;空參數列()走原本的 PARENS 分支 (輸出不含參數清單,維持與 K&R 語意一致:參數未指定)。 - 測試設計:樣本涵蓋——頂層函式、函式指標、陣列元素是函式指標、 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) |