参考:Andrew Koenig《C 陷阱与缺陷(第二版)》2.1节
目录
先看一行代码,不用着急理解:
(*(void (*)())0)();
作者在 2.1 节开头说,这样的表达式"恐怕会令每个 C 程序员的内心都'不寒而栗'"。
它是怎么来的?
书里说,有位程序员在给某种微处理器写程序,那台机器启动时硬件会调用首地址为 0 位置的子例程。他要写一条 C 语句,显式地去调用那个子例程。
结果就是我们看到的这一行。
void(*)()与0这两块似懂非懂,更“要命”的是为什么非要套这么多层括号。还在最外面绕一圈 *?

规则
书里紧接着就把这条规则给了出来:
然而,他们大可不必对此望而生畏,因为构造这类表达式其实只有一条简单的规则:
按照使用的方式来声明。
它的意思是:声明符不是"描述这个变量是什么",而是"描述这个变量在表达式里会怎么被用"。
书里给了两句话解释:
任何 C 变量的声明都由两部分组成:类型以及一组类似表达式的声明符(declarator)。
声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。
以float *pf为例;
float是类型*pf叫声明符——变量名,加上它身上套的符号
”对它求值应该返回一个声明中给定类型的结果“也就是
把声明符当成一个算式去算,算出来的结果,必须正好等于声明开头那个类型。
- 开头写的是
float - 声明符是
*pf,也就是"对 pf 做解引用" - 规则要求:
*pf算出来得是float - 那
pf就不可能是 float 了——它得是"解引用之后能得到 float"的那种东西
结论:pf 是指向 float 的指针。
拿它走一遍
所以读声明的正确方法是:
把变量名为中心,问一句"它在这个表达式里被用完之后,剩下的是什么类型"。
一条一条试:
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
也就是:
找到变量名
↓
看它身上套了什么:
* = 解引用,跟在表达式后的() = 调用,在表达式外面的() = 普通分组括号,[] = 取下标
↓
一层层剥掉,剥完剩下的类型,必须等于声明开头那个类型
至于声明符长得像表达式,不是巧合,是设计,便于阅读。
float *g()与(*h)();
书里把上面那些形式组合起来,给了这么一行:
float *g(), (*h)();
同一行,两个声明符,一个没括号一个有括号。它们完全不是一类东西。
按刚才的规则读:
*g()要先看()。函数运算符()的优先级高于单目*,所以*g()实际上是*(g())。- 也就是"先调用 g,再解引用",结果是 float。
- 倒推:
g是一个函数,返回值类型是指向 float 的指针。
(*h)()里括号把*h圈住了。所以*h是一个函数,h才是指针。- 倒推:
h是一个函数指针,它指向的函数返回 float。
两个形式都一样,差别全在那一对括号上。

机械规则:把声明变成类型转换符
上一节的推法已经了解。书里接着给了一个更省事的机械做法:
一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:
只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个"封装"起来即可。
类型转换符是什么?
类型转换符就是 (类型) 这个东西,写在某个表达式前面。
对编译器说"后面这一坨,按我说的类型处理"。
(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;
} /* 一个普通函数,返回 float */
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是函数名(会自动退化),所以两种都是可以的,进行强转是多余的。但有些情况必须强转
注意这个规则是可逆的。
会读声明,就会写类型转换符;会写类型转换符,也就懂声明了。
回到开头
现在回头看最上面那句。书里的做法是分两步:
第一步:假定变量 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)();

那两个括号为什么不能省
书里这一段说得很严格:
在表达式
(*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 操作数
函数没有大小,函数指针才有大小。
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的函数
就是意思是:
signal 是函数,接受 (int, void (*)(int)),返回一个指针;
这个指针指向一个函数;那个函数接受 int、返回 void。
这里我卡了半小时没反应过来,太难受了,不过最后还是找到关键点了。
用 typedef 简化一下,就好读多了:
typedef void (*HANDLER)(int);
HANDLER signal(int, HANDLER);
转载自 CSDN-专业IT技术社区




