C语言中的变量声明是让程序员比较苦恼的一件事,因为过多的优先级规则使得阅读声明并不能像自然方式那样从左至右的阅读。比如下面这个声明:
int (*(*fun)())();
对于这个声明,你能准确说出它的含义吗?这个声明涉及到本文的两大主题:什么是声明和声明的阅读规则。本文的最后将给出这个声明的准确含义。
声明和定义
在C语言中,提到声明就不得不提到定义。这里说的声明既包含变量的声明又包含函数的声明。对于函数的定义和声明易于理解,比如:
int
myfun2();
int
myfun2()
{
printf
(
"I am myfun2~
"
);
return
0;
}
所谓函数的定义就是对该函数进行具体的功能实现,而函数声明则是对该函数返回值和参数类型的说明,使得其他函数感知到这个函数的存在,以便在需要时直接调用该函数。
变量的声明具体分为两种情况:定义型声明和外部引用型声明。
定义型声明其实就等价于定义。C语言的定义是指为变量分配内存空间,并在需要时为其赋一个初值。定义型声明用于创建一个新的变量,它在定义这个变量的同时也声明了这个变量。比如下述代码就声明(定义)了一个变量num,并将0作为其初值。
int
num = 0;
使用外部引用型声明是由于要在当前程序中使用定义在其他文件中的变量。也就是说这个变量是已存在的,因此这种声明并不包含变量的定义。比如:
file1:
int
a = 100;
file2:
extern
int
a;
C语言中的变量只能有一个定义,但是它却可以有多个extern的声明。因为extern声明并不分配内存空间,只是告诉引用这个变量的函数:这个变量已经定义过了,你可以直接的使用。
声明的组成
声明确定了变量的基本类型和相应的初值(如果需要的话)。一个完整的声明包括三部分:一个类型说明符,一个或多个声明器(declarator)和一个分号。
类型说明符用于描述所要声明变量的类型;分号说明了声明的结束;声明器是标示符以及和它组合在一起的指针符、函数括号和数组下标等,有时候也将初始化内容放在声明器中。多个声明器用逗号隔开。关于声明和声明器的关系可参考下图:
该图所示的语句声明了四个变量,其基本类型都是整型。由于四个变量分别对应着不同的声明器,则最终的变量类型就有所不同。整型变量a的声明器即为标示符a;第二个声明器为*b=NULL,它声明b是一个指针变量,其初值为NULL。由于类型说明符为int,则说明了b是一个指向整型的指针;第三个声明器为(*c)[20],它声明c是一个数组指针,该指针指向拥有20个元素的数组。由类型说明符得知该数组的元素都是整型的;最后一个声明器为*j[20],其说明j是一个指针数组,该数组有20个元素,每个元素都是指向整型变量的指针。
优先级规则
了解了声明的组成后,到了该给出声明优先级规则的时候了。C语言中声明的优先级规则如下:
1.声明从最左的标示符开始,然后按照下面的优先级规则依次读取;
2.具体的优先级为:
- 2.1 被括号括起来的那部分;
- 2.2 后缀部分;如果后缀为( ),表明这是一个函数;如果后缀为[ ],表明这是一个数组;
- 2.3 前缀部分;*表示指向…的指针;
3.如果const或volatile关键字后紧邻基本类型说明符,则它作用于该类型的变量;否则,const和volatile作用于仅靠它左边的星号,即作用于指针变量;
上述规则需要参考实际的声明来慢慢理解。下面通过两个简单的声明举例来说明上述优先级规则:
1 |
const int (*p)(); |
2 |
const int *p(); |
声明1:
- 首先找到标示符p,由于声明1中的p和*被包括起来,根据规则2.1得知p是一个指针。该指针指向什么类型是接下来读声明的关注对象;
- 标示符的后缀部分是( ),根据规则2.2得知该指针指向一个函数。这个函数返回值是什么类型是接下来读声明的关注对象;
- 读完上述声明符号后,剩下了const int,根据规则3得知这个函数的返回值是一个只读型的整数;
综合上面的几部分可得知该声明的含义:p是一个指针,它指向一个函数。这个函数没有参数,返回一个只读型的整数。
声明2:
- 首先找到标示符p,它与*没有被包括在一起,因此可以排除p是一个指针;
- 由于标示符的后缀部分是( ),根据规则2.2得知p是一个函数。该函数返回什么类型是接下来读声明的关注对象;
- 由于p标示符前有*,则说明该函数的返回值是一个指针。至于该指针指向什么样的数据是接下来对声明的关注对象;
- 通过队则3可得知该指针指向一个只读型的整型变量;
通过上述的分步分析可得知该声明的含义:p是一个函数,它没有参数,返回值是一个指针。该指针指向一个只读型的整型变量。
通过上述的优先级规则,可以轻松的阅读任何一个声明。从上述两个举例中也可总结出阅读声明的大致方法,首先判别该声明是声明一个函数还是一个变量;再根据具体的声明类型切换接下来读声明的关注对象;最后未读的基本类型就是最后一次关注对象的类型。上述的大致方法可详细总结如下:
- 1.找到最左边的标示符,表示已被阅读;
- 2.若已阅读符号右方紧邻[,则从左至右阅读到与之配对的 ] 为止。这一段符号表示 当前的关注对象是一个数组,该数组的元素类型未知;接下来的关注对象是数组的元素的类型,转4;否则转至3;
- 3.若已阅读符号右方紧邻(,则从左至右阅读到与之配对的 )为止。这一段符号表示当前的关注对象是一个函数,该函数的返回值类型未知;接下来的关注对象是函数的返回值类型;顺序执行4;
- 4.如果已阅读符号左边的符号是(,则寻找与之配对的 ),这一段符号表示已经阅读过;转至2;否则转至5;
- 5.如果已阅读符号左边的符号是const,volatile和*之一,则不同符号代表不同的含义。const表示只读;volatile表示禁止编译器优化;*表示一个指针,该指针指向那种类型是接下来的关注对象,转4;否则转至6;
- 6.剩下的未阅读的符号为基本的数据类型,即为当前关注对象的数据类型;
通过上述方法即可阅读一个声明,并理解它具体的含义。现在来解决本文一开始的那个声明:
int (*(*fun)())();
其步骤如下:
- 找到标示符fun,fun已被阅读;
- 步骤2,3,4不满足,转到步骤5;fun的左方为*,说明fun是一个指针。当前的关注对象是fun指向什么类型的数据,转到第4步;
- *fun的左方为(,则(*fun)表示已被阅读的符号。当前的关注对象仍然是fun指向什么类型的数据,转到第2步;
- (*fun)的右方紧邻(,则(*fun)()为已阅读的符号。它表示fun指向一个函数,该函数的返回值类型成为接下来的关注对象,转到第4步;
- 步骤4不满足,转到步骤5;(*fun)()的左方为*,说明返回值类型为一个指针,该指针指向什么类型是接下来的关注对象。当前已读过的符号表示fun是一个指针,它指向一个函数,该函数返回一个指针,该指针指向什么类型未知;转至4;
- *(*fun)()左方为(,则(*(*fun)())表示已被阅读。转至第2步;
- (*(*fun)())的右方为(,则(*(*fun)())()为已阅读的符号,它表示当前的关注对象是一个函数。目前为止已阅读的符号表示fun是一个指针,它指向一个函数,该函数返回一个指针,该指针指向一个函数,该函数返回值类型未知;转至4;
- 步骤4,5不满足转至6;int表示当前的关注对象为整型,目前所有的符号已阅读完毕。所有的声明符号表示fun是一个指针,它指向一个函数,该函数返回一个指针,该指针指向一个函数,该函数返回一个整型变量;
大功告成!
上述的分析过程看起来可能有些复杂和呆板,当你熟悉了整个分析过程后,这个过程就变得轻而易举。
举例
结合上面的理论分析,可以阅读下面的代码。这个代码一方面可以帮助你巩固上面的阅读规则,另一方面可以帮助你理解函数声明和指向函数指针的声明两者之间的区别。
/*
* Author: edsionte
* Email: edsionte@gmail.com
* Time: 2011/02/01
*/
#include
int (*myfun1())();
int myfun2();
int (*myfun1())()
{
//myfun1 return a pointer which point a function;
//The return function hasn't argument and reurun a int value;
int (*fun2)();
printf("I am myfun1~
");
fun2 = myfun2;
return fun2;
}
int myfun2()
{
printf("I am myfun2~
");
return 0;
}
int main()
{
int (*(*fun1)())();
int (*fun2)();
fun1 = myfun1;
fun2 = fun1();
fun2();
return 0;
}
参考:
《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;