
1. 引言先看一行代码不用着急理解(*(void (*)())0)();。作者在 2.1 节开头说这样的表达式恐怕会令每个 C 程序员的内心都不寒而栗。它是怎么来的书里说有位程序员在给某种微处理器写程序那台机器启动时硬件会调用首地址为 0 位置的子例程。他要写一条 C 语句显式地去调用那个子例程。结果就是我们看到的这一行。void(*)()与0这两块似懂非懂更要命的是为什么非要套这么多层括号还在最外面绕一圈*规则书里紧接着就把这条规则给了出来然而他们大可不必对此望而生畏因为构造这类表达式其实只有一条简单的规则按照使用的方式来声明。它的意思是声明符不是描述这个变量是什么而是描述这个变量在表达式里会怎么被用。2. 声明符的本质书里给了两句话解释任何 C 变量的声明都由两部分组成类型以及一组类似表达式的声明符declarator。声明符从表面上看与表达式有些类似对它求值应该返回一个声明中给定类型的结果。以float *pf为例float是类型*pf叫声明符——变量名加上它身上套的符号。对它求值应该返回一个声明中给定类型的结果也就是把声明符当成一个算式去算算出来的结果必须正好等于声明开头那个类型。开头写的是float声明符是*pf也就是对 pf 做解引用。规则要求*pf算出来得是float那pf就不可能是float了——它得是解引用之后能得到 float的那种东西。结论pf是指向float的指针。3. 拿它走一遍所以读声明的正确方法是把变量名为中心问一句它在这个表达式里被用完之后剩下的是什么类型。一条一条试float f, g;—— f 求值是 float所以 f 是 float。float ((f));—— 括号随便加跟表达式一样意思是同一个。float ff();—— ff() 求值是 float所以 ff 是函数返回 float。float *pf;—— *pf 求值是 float所以 pf 是指针指向 float。第一个就是最常用的变量声明第二个float ((f))括号只是把同一个东西括起来跟((3))就是 3 一样什么都没改变。第三个float ff();ff()算出来是 float括号这个动作叫调用调用完得到 float。也就是找到变量名看它身上套了什么*是解引用跟在表达式后的()是调用在表达式外面的()是普通分组括号[]是取下标。一层层剥掉剥完剩下的类型必须等于声明开头那个类型。至于声明符长得像表达式不是巧合是设计便于阅读。4. float *g() 与 (*h)()书里把上面那些形式组合起来给了这么一行float *g(), (*h)();。同一行两个声明符一个没括号一个有括号。它们完全不是一类东西。按刚才的规则读*g()要先看()。函数运算符()的优先级高于单目*所以*g()实际上是*(g())。也就是先调用 g再解引用结果是 float。倒推g 是一个函数返回值类型是指向 float 的指针。(*h)()里括号把*h圈住了。所以*h是一个函数h 才是指针。倒推h 是一个函数指针它指向的函数返回 float。两个形式都一样差别全在那一对括号上。5. 机械规则把声明变成类型转换符上一节的推法已经了解。书里接着给了一个更省事的机械做法一旦我们知道了如何声明一个给定类型的变量那么该类型的类型转换符就很容易得到了只需要把声明中的变量名和声明末尾的分号去掉再将剩余的部分用一个括号整个封装起来即可。类型转换符是什么类型转换符就是(类型)这个东西写在某个表达式前面对编译器说后面这一坨按我说的类型处理。(int)3.7—— 把 3.7 按 int 处理得到 3。(float)5—— 把 5 按 float 处理得到 5.0f。(char)300—— 把 300 按 char 处理得到 44。(void (*)())0—— 把 0 按函数指针处理得到一个地址。之后以float (*h)();为例float (*h)(); /* 原声明 */ float (*)(); /* 去掉变量名 h 和末尾分号 */ (float (*)()) /* 整体用括号封装 —— 这就是类型转换符 */所以(float (*)())的意思就是指向返回 float 的函数的指针。实测这条规则确实能直接用#include stdio.h float ff(void) { return 1.5f; } int main(void) { /* 带类型转换符的写法 */ float (*q)(void) (float (*)(void))ff; /* q 是一个函数指针——指向不接受参数、返回 float的函数 */ printf(q() %f\n, q()); /* () 的操作数本来就该是函数指针 */ /* 不带类型转换符的写法一样能跑函数名会自动退化成指针 */ float (*r)(void) ff; printf(r() %f\n, r()); return 0; }这里的ff是函数名会自动退化所以两种都是可以的进行强转是多余的。但有些情况必须强转。注意这个规则是可逆的。会读声明就会写类型转换符会写类型转换符也就懂声明了。6. 回到开头现在回头看最上面那句。书里的做法是分两步第一步假定变量fp是一个函数指针怎么调用它指向的函数(*fp)();。因为fp是指针*fp就是它指向的函数所以(*fp)()是在调用那个函数。书里在这里专门提醒了一句ANSI C 标准允许程序员将上式简写为fp()但是一定要记住这种写法只是一种简写形式。第二步把fp替换掉。先试最朴素的写法(*0)();不行。因为*需要一个指针当操作数而0是整数error C2100: 无法取消引用类型为int的操作数 error C2064: 项不会计算为接受 0 个参数的函数所以必须对0先做类型转换转成指向返回值为 void 类型的函数的指针。那个类型怎么拼用上一节的机械规则反推——从void (*fp)();去掉fp和分号、加括号void (*)() /* 类型转换符 (void (*)()) */ (void (*)())0 /* 把 0 转成这个类型 */替换进去得到开头那一行(*(void (*)())0)();。也就是把整数 0 强制转换为不接受参数、返回 void 的函数指针解引用该指针得到函数调用该函数。书里也说还可以用 typedef 让它好读一点typedef void (*funcptr)(); /* 定义 funcptr 是 void (*)() 类型 */ (*(funcptr)0)();7. 那两个括号为什么不能省书里这一段说得很严格在表达式(*fp)()中*fp两侧的括号非常重要因为函数运算符()的优先级高于单目运算符*。如果*fp两侧没有括号那么*fp()实际上与*(fp())的含义完全一致。写法读法意思(*fp)()先解引用再调用调用 fp 指向的函数*fp()先调用再解引用调用 fp 这个函数然后对返回值解引用括号不是装饰它决定*落在谁身上。再补一条能彻底分开函数名和函数指针的sizeof。printf(sizeof(pf) %u\n, (unsigned)sizeof(pf)); /* 8 */ unsigned n (unsigned)sizeof(ff); /* 编译不过 */sizeof(pf) 8 error C2070: float (__cdecl *)(void): 非法的 sizeof 操作数函数没有大小函数指针才有大小。8. signal 那个著名声明书里拿这个例子收尾因为它不是孤例。signal 库函数有两个参数一个整型的信号编号一个指向用户信号处理函数的指针。它的返回值也是这种指针。先用刚才的规则写指向处理函数的指针void (*sfp)(int); /* sfp 是指向接受 int、返回 void 的函数的指针 */然后推 signal 的完整声明void (*signal(int, void (*)(int)))(int);之前见到的返回值表示都是这样的int f(void);—— 返回 int。int *f(void);—— 返回 int *。char **f(void);—— 返回 char * *。struct Point f(void);—— 返回 struct Point。就是返回类型在前但返回值是一个函数的指针的情况就不能这样表示了void (*)(int) signal(int); /* 错误写法 */因为*被包住了只能这样void (*signal(int, void(*)(int)))(int);拆开看void (*)(int)是返回类型signal()是函数声明函数名与参数int, void (*)(int)是一个参数是 int一个参数是一个指针——指向一个接受 int 返回 void 的函数。