第 13 章 · 复杂指针声明解析

每个 C 学习者都会在某一天撞上这样一行字:void (*signal(int, void (*)(int)))(int);。这不是乱码,是标准库 signal 函数的真实原型,而它读懂只需要两件武器:一套读法规则(右左法则)和 typedef 拆解。本章把这两件武器配齐,顺便承认一个工程事实:看懂复杂声明是素养,不写复杂声明是美德。原文小节编号错成 12.x,一并修正。

13.1 优先级:声明的语法骨架

声明里的符号有优先级,谁先结合谁说了算:

  1. 后缀 [](数组)、()(函数):最高;
  2. 前缀 *(指针):次之;
  3. 显式括号:改变结合顺序的唯一手段。

这一条就解开了贯穿前几章的谜团:int *f(int) 里 f 先和后缀 () 结合(f 是函数),再和 * 结合(返回 int*);int (*f)(int) 里括号强迫 f 先和 * 结合(f 是指针),再指向函数。前面所有「指针数组 vs 数组指针」「函数 vs 函数指针」的分辨,全靠这条规则,现在有了统一的解释。

13.2 右左法则:从名字出发的阅读路线

规则的完整版:从标识符出发,先向右、后向左,遇括号拐弯,把遇到的符号依次翻译:

  • 向右遇到 []:「……的数组」;遇到 ():「……的函数」;
  • 向左遇到 *:「……的指针」;
  • 碰到配对括号,跳进去读完再出来。

拿本章开头的天书热身,先换一个中等难度的 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 是函数(右)→ 返回值是指针 → 指向函数返回函数指针的函数

13.3 signal:标准库里的boss

现在正面强攻:

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。

13.4 工程立场:读得懂 ≠ 要写

本章最后立一条实践规范,按重要性排:

  1. 自己写代码时,用 typedef 或结构体把复杂类型拆碎。超过两层嵌套(T (*)[N] 算一层)的裸声明就该重构了。
  2. 读别人的天书时,右左法则 + 逐步 typedef 誊写,别硬瞪。
  3. 怀疑 «指针天书» 的必要性。绝大多数天书可以被 struct { 函数指针成员 }(第 12 章)或干脆改接口消灭;signal 那种是历史包袱,不是值得模仿的风格。

现代 C(C23)还给 auto 存储类和 typeof 提供了更多藏起类型细节的手段,C++ 更是直接用 auto/function 把这些全部封装。但底层库里天书仍会时不时冒出来,这章的功夫就是为那一刻准备的。

常见错误

  • 把右左法则背成口诀却不走括号。括号是规则里唯一能「改变命运」的符号,漏拐弯必读错;拿不准就在纸上把每个符号按结合顺序编号。
  • typedef 的位置放错。typedef int (*Handler)(char*) 里 Handler 替换的是「(* Handler)」这个整体;写错位置(如 typedef int* Handler(char*))得到的是函数类型不是指针类型,报错信息又长又迷。
  • 用 cdecl 的能力而不自知。复杂声明可以丢给 cdecl.org 这类工具验证读法,工程里别跟自己的脑容量较劲。

练习

  1. 手算:用右左法则读出下列声明的完整含义:char *(*(*p)[3])(char *)(提示:数组 → 指针 → 函数指针)。
  2. 动手:把第 1 题的声明用 typedef 拆成三步,每步编译验证一次,最终写出等价的三行 typedef 版代码。
  3. 手算:写出「一个函数,参数是指针数组(char* 的数组首地址),返回函数指针,指向的函数参数为 int 返回 void」的声明;再用 typedef 改写成可读版本。
  4. 思考:C 为什么让声明长得像「使用表达式」(int *p 的 * 长得像解引用)?这个设计的历史理由(K&R 时代的「声明模仿使用」)今天看是资产还是负债?找一两个现代语言(Go 的 p *int、Rust 的 &mut i32)对比它们如何处理「指针类型声明」。