UIU114头像
(*(void (*)())0)(); 在干什么?一条规则读懂 C 声明封面图

(*(void (*)())0)(); 在干什么?一条规则读懂 C 声明

参考:Andrew Koenig《C 陷阱与缺陷(第二版)》2.1节

目录

规则

float *g()与(*h)();

机械规则:把声明变成类型转换符

回到开头

那两个括号为什么不能省

signal 那个著名声明


先看一行代码,不用着急理解:

(*(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 指向的函数

*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技术社区

原文链接:https://blog.csdn.net/UIU114/article/details/166372210

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--