C语言的声明模型之所以如此晦涩,这是有历史原因的。六十年底晚期,人们在设计C语言时,“类型模型”概念对当时的编程语言理论而言尚属陌生。BCPL(C语言的祖先)几乎没有类型,它把二进制作为唯一的数据类型,所以C语言有先天性的缺陷。然后出现了一种C语言设计哲学,要求对象的声明形式与它的使用形式尽可能相似。一个int类型的指针数据被声明int *p[3];并以*p[i]这样的表达式引用或使用指针所指向的int数据,所以它的声明形式和使用形式非常相似。这样做的好处是各种不同操作符的优先级在“声明”和“使用”时是一样的。它的缺点在于操作符的优先级(有15级或更多,取决于你怎么算)是C语言中另外设计不当。过于复杂之处。程序员需要记住特殊的规则才能推断出int *p[3]到底是一个int类型的指针数组,还是一个指向int数组的指针。
“声明的形式和使用的形式相似”这种用法是C语言的独创,其他语言并没有采取这种方法。而且,“声明的形式和使用的形式相似”即使在当时也不像是一个特别好的主意。C语言的声明存在的最大问题是你无法以一种人们所习惯的自然方式从左到右阅读一个声明,在ANSI C引入volatile和const关键字后,情况就更加复杂了。由于这些关键字只能出现在声明中(而不是使用中),这就使得现今声明形式和使用形式完全对的上号的列子越来越少了。那些从风格上看像是声明,但却没有标识符的东西(如形式参数声明和强制类型转换)看上去让人费解。如果想要把什么东西的类型强制转换为指向数组的指针,就不得不使用下面的语句来表示这个强制类型转换:
char (*j)[20]; //j是一个指向数组的指针,数组内有20个char元素
j = (char (*)[20]) malloc(20);
如果把星号两边看上去明显多于的括号拿掉,代码会变成非法的。
涉及指针和const的声明可能会出现几种不同的顺序:
const int * grape;
int const * grape;
int const * const grape_jam;
在最后一种情况下,指针是只读的,而在另外两种情况下,指针所指向的对象是只读的。当然对象和指针有可能都是只读的,如下声明:
const int * const grape_jam;
int const * const grape_jam;
对于更复杂的语法形式又将如何呢?如下面的声明(取自telnet程序)。
char * const *(*next)();
1、声明是如何形成的
我们先来看一些C语言的术语以及一些能组合成一个声明的单独语法成份。其中一个非常重要的成分就是声明器 - 它是所有声明的合心。简单的说,声明器就是标识符以及与它组合在一起的任何指针、函数括号、数组下表等,如下图所示。
一个声明由下图所示的各个部分组成。声明确定了变量的基本类型以及初始值(如果有的话)。
让我们看一下如果使用这些部件来构造一个声明,情况能够复杂到什么程度。同事要记住,在合法的声明中存在限制条件。
(1) 函数的返回值不能是一个函数,像foo()()这样是非法的。
(2) 函数的返回值不能是一个数组,像foo()[]这样是非法的。
(3) 数组里面不能有函数,像foo[]这样是非法的。
但像下面这样声明则是合法的:
(1) 函数的返回值允许是一个函数指针,如:int(*fun())();
(2) 函数的返回值允许是一个指向数组的指针,如int(* foo())[];
(3) 数组里面允许有函数指针,如int(* fool[])();
(4) 数组里面允许有其他数组,所以能经常看到int foo[][]。
2、优先级规则
要理解一个声明,必须要懂得其中的优先级规则。C语言的优先级规则如下:
A 声明从它的名字开始读取,然后按照优先级顺序依次读取。
B 优先级从高到底依次是:
B. 1 声明中被括号括起来的那部分
B 2 后缀操作符:括号()表示一个函数,而方括号[]表示一个数组。
B 3 前缀操作符:星号*表示“指向…的指针”。
C 如果const和(或)volatile关键字的后面紧跟说明符(如int,long等),那么
它作用于类型说明符、在其他情况下,const和(或)volatile关键字作用于它
左边紧邻的指针星号。
现在我们利用优先级规则来分析前面提到的声:
char * const *(*next)();
A 首先,看变量名“next”,并注意到它直接被括号所括住
B.1 所以先把括号里的东西作为一个整体,得出“next是一个指向…的指针”
B 然后考虑括号外面的东西,在星号前缀和括号后缀之间做出选择
B.2 B.2规则告诉我们优先级较高的是右边的函数括号,所以“next是一个函数函数指
针,指向一个返回…的函数”
B.3 然后,处理前缀“*”,得出指针所指的内容
C 最后,把“char * const”解释为指向字符的常量指针
把上面分析结果加以概括,这个声明表示“next是一个指针,它指向一个函数,该函数返回另一个指针,该指针指向一个类型为char的常量指针”。下图则是一种更加直观的方法:
C语言中的声明读起来并没有固定的方向,一会从左读到右,一会又从右读到左。一开始,我们从左边开始向右寻找,直到找到第一个标识符。当声明中的某个符号与图中所示匹配时,便把它从声明中处理掉,以后不再考虑。在具体的每一步骤上,我们首先查看右边的符号,再看左边。所有的符号处理完后就大功告成。
3、一个实例
通过上面的分析,我们可以再分析一个更加复杂的例子。
char *(* c[10])(int **p);
现在我们来分析,首先从左可似乎往右看,先找到标识符c,右边比左边的优先级高,所以先结合右边[10],c[10]表示的是一个元素数量为10的数组,然后在结合左边的,所以( c[10])表示一个指针数组,在结合右边的(int p),说明这是一个指向函数的指针数组,这个函数的参数为p,参数类型为int,最后结合左边,可以知道这是个指针数组,其指向一个函数,该函数的返回值是一个指向char类型的指针。
参考资料:《C专家编程》