要理解一个C程序,仅仅理解组成该程序的符号是不够的。程序员还必须理解这些符号是如何组合成声明、表达式、语句和程序的。
下面我们来举个例子,当计算机启动时,硬件将调用首地址为0位置的子例程。为了模拟开机启动的情形,我们必须设计出一个C语句,以显式调用该子例程。该语句如下:
(*(void(*) ())0)();
对于这样的一个表达式,大家可能会不理解,望而生畏。但要注意的是构造这类表达式只有一条简单的规则:按照使用的方式来声明。任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符(declarator)。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。最简单的声明符就是单个变量,如:float f, g; 这个声明的含义是:当对其求值时,表达式f和g的类型为浮点数类型(float)。因为声明符与表达式相似,所以我们也可以在声明符中任意使用括号:float ((f));这个声明的含义是:当对其求值时,((f))的类型为浮点类型,由此可以推知,f也是浮点类型。
同样的逻辑也适用于函数和指针类型的声明,例如:float ff(); 这个声明的含义是:表达式ff()求值结果是一个浮点数,也就是说ff是一个返回值为浮点类型的函数。同样的float *pf,这个声明的含义就是*pf求值后为一个浮点数,所以pf就是一个指向一个浮点数的指针。类推就可以得到float *g(), (*h)(); 这个声明的含义读者可以自己去思考。
一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。例如:
float (*h)();
而float (*)()表示一个“指向返回值为浮点类型的函数的指针”的类型转换符。
了解上面提到的,现在我们就可以来分析(*(void(*) ()0))。首先我们先来看一个函数指针的调用:
(*fp)();
ANSIC标准允许程序员将上式简写为fp(),但是一定要记住这种写法只是一种简写形式。此外,在表达式(*fp)()中,*fp两侧的括号是非常重要的,否则的话*fp()与(*fp)()的含义就是相同的了,但其实*fp()是*((*fp)())的简写形式。而此函数的返回值为void的声明为:
void (*fp)();
接下来,我们找到一个恰当的表达式来替换fp。如果我们能找到一个表达式来替换fp,而且能让表达式中的函数指针的地址为0,这样我们的目标就达到了。由前面的类型转换符,我们已经知道了如何对一个常数进行类型转换,将常数0转换为“批向返回值为void的函数指针”类型,可以这样写:
(void(*) ())0
接下来我们把fp换成上式即可得到:
(*(void(*) ())0)();
如果使用了C语言中的typedef,则上式又可以变为下列形式:
typedef void (*funcptr)(); (*(funcptr)0)();
这样我们就可以更加清晰地理解了。接下来我们再扩展一下,讨论另一个问题,如果有一个函数返回的是一个函数指针,那我们应该怎样去使用这个返回的函数呢?以signal函数为例,signal函数接受两个参数:一个是代表需要“被捕获”的特定signal的整数值;另一个是指向用户提供的函数的指针,该函数用于处理“捕获到”的特定的signal,返回值类型为void。也就是signal的形式如下:
signal(int, void(*)(int));
我们已经理解了void (*sfp)(int)形式,再考虑到signal返回的是一个函数指针,那就只要把sfp置换掉就可以了,得到:
void (*signal(int, void(*) (int))) (int);
上面的声明可以这样理解:传递适当的参数以调用signal函数,对signal函数返回值(为函数指针类型)解除引用(dereference),然后传递一个整形参数调用解除引用后所得函数。
未经允许不得转载:TacuLee » C陷阱与缺陷之理解函数声明