5.12 复杂声明
C 语言常常因为声明的语法问题而受到人们的批评,特别是涉及到函数指针的语法
C 语言的语法力图使声明和使用相一致
对于简单的情况,C 语言的做法是很有效的,但是,如果情况比较复杂,则容易让人混淆
原因在于,C 语言的声明不能从左至右阅读,而且使用了太多的圆括号
我们来看下面所示的两个声明:
int *f(); /* f: function returning pointer to int */
以及
int (*pf)(); /* pf: pointer to function returning int */
它们之间的含义差别说明:
*
是一个前缀运算符,其优先级低于 ()
,所以,声明中必须使用圆括号以保正确的结合顺序
尽管实际中很少用到过于复杂的声明,但是,懂得如何理解甚至如何使用这些复杂的声明是很重要的
创建复杂声明的一种比较好的方法是,使用 typedef
通过简单的步骤合成,这种方法我们将在 6.7 节中讨论
这里介绍另一种方法,接下来讲述的两个程序就使用这种方法:
一个程序用于将正确的 C 语言声明转换为文字描述,另一个程序完成相反的转换
文字描述是从左至右阅读的
第一个程序 dcl
复杂一些,它将 C 语言的声明转换为文字描述:
char **argv
argv: pointer to char
int (*daytab)[13]
daytab: pointer to array[13] of int
int *daytab[13]
daytab: array[13] of pointer to int
void *comp()
comp: function returning pointer to void
void (*comp)()
comp: pointer to function returning void
char (*(*x())[])()
x: function returning pointer to array[] of
pointer to function returning char
char (*(*x[3])())[5]
x: array[3] of pointer to function returning
pointer to array[5] of char
程序 dcl
是基于声明符的语法编写的
附录 A 以及 8.5 节将对声明符的语法进行详细的描述
下面是其简化的语法形式:
dcl: optional *'s direct-dcl
direct-dcl name
(dcl)
direct-dcl()
direct-dcl[optional size]
简而言之,声明符(dcl
)就是前面可能带有多个 *
的直接声明符(direct-dcl
)
direct-dcl
可以是 name
、由一对圆括号括起来的 dcl
、
后面跟有一对圆括号的 direct-dcl
、后面跟着用方括号括起来的表示可选长度的 direct-dcl
该语法可用来对 C 语言的声明进行分析
例如声明符 (*pfa[])()
按照该语法分析,pfa
将被识别为一个 name
,从而被认为是一个 direct-dcl
于是,pfa[]
也是一个 direct-dcl
接着,*pfa[]
被识别为一个 dcl
,因此,判定 (*pfa[])
是一个 direct-dcl
再接着,(*pfa[])()
被识别为一个 direct-dcl
,因此也是一个 dcl
可以用下图所示的语法分析树来说明分析的过程(其中 direct-dcl
缩写为 dir-dcl
)
程序 dcl
的核心是两个函数:dcl
与 dirdcl
,它们根据声明符的语法对声明进行分析
因为语法是递归定义的,所以在识别一个声明的组成部分时,这两个函数是相互递归调用的
我们称该程序是一个递归下降语法分析程序
/* dcl: parse a declarator */
void dcl(void)
{
int ns;
for (ns = 0; gettoken() == '*'; ) /* count *'s */
ns++;
dirdcl();
while (ns-- > 0)
strcat(out, " pointer to");
}
/* dirdcl: parse a direct declarator */
void dirdcl(void)
{
int type;
if (tokentype == '(') { /* ( dcl ) */
dcl();
if (tokentype != ')')
printf("error: missing )\n");
} else if (tokentype == NAME) /* variable name */
strcpy(name, token);
else
printf("error: expected name or (dcl)\n");
while ((type = gettoken()) == PARENS || type == BRACKETS)
if (type == PARENS)
strcat(out, " function returning");
else {
strcat(out, " array");
strcat(out, token);
strcat(out, " of");
}
}
该程序的目的旨在说明问题,并不想做得尽善尽美,所以对 dcl
有很多限制
它只能处理类似于 char
或 int
这样的简单数据类型,而无法处理函数中的参数类型或类似于 const
这样的限定符
它不能处理带有不必要空格的情况
由于没有完备的出错处理,因此它也无法处理无效的声明
下面是该程序的全局变量和主程序:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define MAXTOKEN 100
enum { NAME, PARENS, BRACKETS };
void dcl(void);
void dirdcl(void);
int gettoken(void);
int tokentype; /* type of last token */
char token[MAXTOKEN]; /* last token string */
char name[MAXTOKEN]; /* identifier name */
char datatype[MAXTOKEN]; /* data type = char, int, etc. */
char out[1000];
main() /* convert declaration to words */
{
while (gettoken() != EOF) { /* 1st token on line */
strcpy(datatype, token); /* is the datatype */
out[0] = '\0';
dcl(); /* parse rest of line */
if (tokentype != '\n')
printf("syntax error\n");
printf("%s: %s %s\n", name, out, datatype);
}
return 0;
}
函数 gettoken
用来跳过空格与制表符,以查找输入中的下一个记号
“记号”(token
)可以是一个名字,一对圆括号,可能包含一个数字的一对方括号,也可以是其它任何单个字符
int gettoken(void) /* return next token */
{
int c, getch(void);
void ungetch(int);
char *p = token;
while ((c = getch()) == ' ' || c == '\t')
;
if (c == '(') {
if ((c = getch()) == ')') {
strcpy(token, "()");
return tokentype = PARENS;
} else {
ungetch(c);
return tokentype = '(';
}
} else if (c == '[') {
for (*p++ = c; (*p++ = getch()) != ']'; )
;
*p = '\0';
return tokentype = BRACKETS;
} else if (isalpha(c)) {
for (*p++ = c; isalnum(c = getch()); )
*p++ = c;
*p = '\0';
ungetch(c);
return tokentype = NAME;
} else
return tokentype = c;
}
有关函数 getch
和 ungetch
的说明,参见第 4 章
如果不在乎生成多余的圆括号,另一个方向的转换要容易一些
为了简化程序的输入,我们将 "x is a function returning a pointer to an array of pointers to functions returning char"
(x
是 一个函数,它返回一个指针,该指针指向一个一维数组,该一维数组的元素为指针,这些指针分别指向多个函数,这些函数的返回值为 char
类型)
的描述用 x () * [] * () char
形式表示
程序 undcl
将把该形式转换为 char (*(*x())[])()
由于对输入的语法进行了简化,所以可以重用上面定义的 gettoken
函数
undcl
和 dcl
使用相同的外部变量
/* undcl: convert word descriptions to declarations */
main()
{
int type;
char temp[MAXTOKEN];
while (gettoken() != EOF) {
strcpy(out, token);
while ((type = gettoken()) != '\n')
if (type == PARENS || type == BRACKETS)
strcat(out, token);
else if (type == '*') {
sprintf(temp, "(*%s)", out);
strcpy(out, temp);
} else if (type == NAME) {
sprintf(temp, "%s %s", token, out);
strcpy(out, temp);
} else
printf("invalid input at %s\n", token);
}
return 0;
}