C++迈向精通:理解函数声明,编译器是如何看懂声明的?

理解函数声明

首先先来看一个代码:

(*(void(*)())0)();

这个代码的含义是:调用首地址为0位置的子例程(函数)
这个代码相信大部分人应该是望而生畏的,但是实际上想要理解这种表达式并不难。

声明的深度理解

其实任何C变量的声明只有两个部分组成:

  • 类型
  • 一组类似表达式的声明符

其中,声明符从表面上看与表达式类似,对它求值时应当返回一个对应类型的结果
在此之前,必须说一句,变量名和声明符号是分开的。
什么意思呢?我们举出几个例子就知道了:

例子

float f, g;

按照上面的理解,这个生命应分为两部分:

  • 类型:float
  • 声明符:f,g

那么这个声明的含义就很明显了:
当对f或者g求值时,他们的返回值应当是 float 类型的。

是不是很简单?

再看几个例子:

float ((f)); // ((f))的类型为浮点类型,可以推断f就是浮点型
float ff();  // ff()的求值结果是一个浮点数,也就是说,ff是一个返回值为浮点类型的函数
float *pf;   // pf的返回值是一个指向浮点数的指针

不管是函数还是指针类型,都是一样的逻辑。
我们可以尝试将他们组合起来:

float *g(), (*h)();

其中 *g() 可以看作 (*g()) 这是因为 * 的结合优先级低于小括号,因此可以看出这个声明的含义是声明了一个函数,参数为空,返回值是个指针,指针类型为 float
而后面的这个就与前面不同,因为使用括号将星号和名称括了起来,因此他的含义是声明了一个变量,变量类型是函数指针,函数指针的返回值为 float , 传入参数为空。

还记得一开始我说那句话吗:

变量名和声明符号是分开的。

我必须要对此进行解释,这样你才有可能真正理解编译器是如何解析声明语句的。

变量名与声明符号

还是这个例子:

float *g(), (*h)();

首先,我们必须理解,声明的对象是变量。也就是说,无论你添加或者减少多少符号上去,变量的名字始终是主要的。(如果你看不懂这句话,那么请继续向下看)

例如第一个变量:
首先搞清楚他的变量名是什么,显而易见是 g
根据我们一开始提到的:

任何C变量的声明只有两个部分组成:类型、一组类似表达式的声明符

那么类型就是 float 了, 剩下的部分都是声明符号。

然后请你记住:声明符号是可以任意拆分的。(前提是合法)

那么,变量 g 的声明符号应该是: *()
加上返回值类型就构成了 float *()
因此,float *()表示一个“返回值为指向浮点类型的指针的函数”的类型转换符(这个类型转换符号用于转换变量)。

同理,变量 h 的声明符号应该是: (*)()
加上返回值类型就构成了 float (*)()
因此,float (*)() 表示一个“返回值为浮点类型的函数的指针”的类型转换符。

分析 (\*(void(\*)())0)();

有了上面的知识储备,我们就可以来尝试分析这个表达式的含义了:

可以分成两步:

第一步

假定变量 fp 是一个函数指针,那么 *fp 就是该指针所指向的函数,所以 (*fp)() 就是调用该函数的方式。

ANSI C标准允许程序员将上式简写为 fp() 但请你记住,这种写法只是一种简写形式。

在表达式 (*fp)() 中, *fp 两侧的括号非常的重要,因为函数运算符 () 的优先级高于单目运算符 * 。如果 *fp 的两侧没有括号,那么 *fp 实际上与 *(fp()) 没有区别。ANSI C把它作为 *((*fp)()) 的简写形式。

第二步

到这一步,我们已经可以理解一半文章开始的那个例子了:

(*(void(*)())0)();

尝试将 (void(*)())删掉,那么这个式子就会变成:

(*0)();

这个式子并不能生效,因为 * 运算符必须用一个指针来作为操作数。不仅如此,这个指针在这个式子中应当是一个函数指针。
因为我们的目的是调用首地址为0位置的子例程,而0很显然是个地址,但并不是个函数指针,所以我们需要将其转换为函数指针类型,转换的类型可以描述为“指向返回值为void类型的函数的指针”。
这就是我们去掉的那一部分(void(*)())的含义。

如果 fp 是一个指向返回值为 void 类型的指针,那么 (*fp)() 的值为 voidfp 的声明如下:

void (*fp)();

因此,我们可以使用下面的方式来尝试调用存储位置为0的例程:

void (*p)(); // declaration
(*p)(); // call

上面的写法并不合“规矩”,因为这种写法假设fp默认初始化为0

实际上应当这样写:

void (*p)(); // declaration
p = 0x00000000; // 假设是32位操作系统
(*p)(); // call

如果你使用 typedef 的话,很显然能更清晰的说明这个问题:

typedef void (*funcptr)();
(*(funcptr)0)();

例子二

实际上有个更加复杂的例子,那就是 <signal.h> 库中的 signal 函数。

首先来说明一下signal函数的用法:

传入两个参数,第一个参数是一个 int 类型的变量表示需要“被捕获”的特定信号整数值,第二个参数是一个函数指针,表示捕获到信号之后的处理函数,这个函数的返回值为void

在man手册中,声明的方式是这样的:

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

但是如果没有typedef,这个声明将会是这样的:

			void (*signal(int, void(*)(int)))(int);

我们理解这个函数需要一些时间,但是这个问题并不是那么难,只要掌握了对应的方法。

我们从用户定义处理函数开始理解结社这个函数可以定义成下面的样子:

void functackle(int n){ ... }

那么不难想象,他的函数指针应当是这个样子:

void (*funcp)(int);

知道了函数指针的样子,很容易就能提取出他的声明符号:

void (*)(int);

根据 signal 函数的定义,它的返回值应该也是这个样子,因此,我们需要对函数的返回值进行符号修饰,首先不管 signal 函数的参数是什么,其最终应当作为一个变量存在,我们设这个变量为 x
如果要对 x 进行修饰,我们将返回值的声明描述为“返回值为void,参数为int的函数指针”,就需要这样:

void (*x)();

然后把x替换为signal函数:

void (*signal(...))();

这就是signal函数的返回值声明,最后我们将参数添加其中就可以了:

void (*signal(int, void(*)()))(int);

这就是 signal 函数的声明

练习

尝试不用 typedef 声明如下的变量:

  • 返回值为 int ,传入参数为函数指针的变量k
  • 返回值为int, 传入参数为一个int 和一个函数指针的变量p
  • 返回值为一个函数指针,该函数指针的传入参数为 int 和 double,传入参数为 int 的变量q
  • 返回值为一个函数指针,该函数指针的传入参数为 int 和 一个返回值为void,传入参数为空的函数指针,传入参数为 int 的变量s
  • 返回值为一个函数指针,(该函数指针的返回值为一个返回值为long long,传入参数为int的函数指针),传入参数为两个函数指针,两个函数指针的返回值类型为long long ,参数都为int 和 double, 变量名为x

如果你真的都会了,那么 congratulations!!!你完全理解了C/C++中的声明方式!!!

这里揭晓最后一问的答案:

long long (*(*x)(long long(*)(int, double), long long(*)(int, dounle)))(int);
  • 24
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若亦_Royi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值