每个 C 学习者都会在某一天撞上这样一行字:void (*signal(int, void (*)(int)))(int);。这不是乱码,是标准库 signal 函数的真实原型,而它读懂只需要两件武器:一套读法规则(右左法则)和 typedef 拆解。本章把这两件武器配齐,顺便承认一个工程事实:看懂复杂声明是素养,不写复杂声明是美德。原文小节编号错成 12.x,一并修正。
声明里的符号有优先级,谁先结合谁说了算:
[](数组)、()(函数):最高;*(指针):次之;这一条就解开了贯穿前几章的谜团:int *f(int) 里 f 先和后缀 () 结合(f 是函数),再和 * 结合(返回 int*);int (*f)(int) 里括号强迫 f 先和 * 结合(f 是指针),再指向函数。前面所有「指针数组 vs 数组指针」「函数 vs 函数指针」的分辨,全靠这条规则,现在有了统一的解释。
规则的完整版:从标识符出发,先向右、后向左,遇括号拐弯,把遇到的符号依次翻译:
[]:「……的数组」;遇到 ():「……的函数」;*:「……的指针」;拿本章开头的天书热身,先换一个中等难度的 int *(*f[5])(char *):
f f 是……
[5] ……5 个元素的数组(右)
* ……数组元素是指针(左)
(char *) ……指向「参数为 char* 的函数」(右)
int * ……返回 int*(左)
结论:f 是「5 个元素的数组,元素是指向『(char*)→int* 函数』的指针」
对照表(前几章的老朋友,现在都能机械地读出来):
| 声明 | 右左读法 | 身份 |
|---|---|---|
int *p[5] | 数组(右)→ 指针(左) | 指针数组 |
int (*p)[5] | 括号内先读:p 是指针 → 指向 5 个 int 的数组 | 数组指针 |
int (*f[5])(int) | 数组 → 元素是指针 → 指向函数 | 函数指针数组 |
int (*f(int))(char) | f 是函数(右)→ 返回值是指针 → 指向函数 | 返回函数指针的函数 |
现在正面强攻:
void (*signal(int sig, void (*func)(int)))(int);
右左法则走一遍:signal 是函数(右边紧跟的括号);参数是 int 和一个函数指针(括号内部再走一遍右左:func 是指针、指向「参数 int、返回 void」的函数);返回值,跳到外层 (* ... )(int),是指向「参数 int、返回 void」函数的指针。合起来:signal 是个函数,收一个信号编号和一个处理函数,返回旧的处理函数。语义马上就通了(信号处理可以嵌套,所以要旧函数可还),这个设计还藏着 C 的一个坑:返回的处理函数链意味着你要保存返回值以便恢复。
typedef 拆解后,天书现出人话:
typedef void (*sighandler_t)(int); // 信号处理函数的类型
sighandler_t signal(int sig, sighandler_t func); // 一眼看懂
这也是工程实践的标准答案:中间类型值得一个名字。typedef 不创造新类型,只是给「某个完整类型」起别名,但它把可读性从 0 拉到 100。
本章最后立一条实践规范,按重要性排:
T (*)[N] 算一层)的裸声明就该重构了。struct { 函数指针成员 }(第 12 章)或干脆改接口消灭;signal 那种是历史包袱,不是值得模仿的风格。现代 C(C23)还给 auto 存储类和 typeof 提供了更多藏起类型细节的手段,C++ 更是直接用 auto/function 把这些全部封装。但底层库里天书仍会时不时冒出来,这章的功夫就是为那一刻准备的。
typedef int (*Handler)(char*) 里 Handler 替换的是「(* Handler)」这个整体;写错位置(如 typedef int* Handler(char*))得到的是函数类型不是指针类型,报错信息又长又迷。char *(*(*p)[3])(char *)(提示:数组 → 指针 → 函数指针)。p *int、Rust 的 &mut i32)对比它们如何处理「指针类型声明」。