5.12 复杂声明
C 语言有时会因为声明的语法而受到谴责,特别是涉及函数指针的声明语法。语法试图使声明和使用一致;在简单的情况下它的效果不错,但在更复杂的情况下会让人困惑,因为声明不能从左往右读,而且括号被过度使用了。如下两个声明
int *f(); /* f:返回int指针的函数 */
和
int (*pf)(); /* pf:指向返回int的函数的指针 */
它们之间的差异就说明了这个问题:* 是前缀操作符且优先级比括号低,为了强制得到正确的关联就需要括号。
尽管真正复杂的声明在实际工作中很少出现,但明白如何理解复杂声明,以及在有需要时知道如何创建复杂声明,都是很重要的。用 typdef 在几个小步骤中合成声明是一种不错的方式,将会在 6.7 节讨论。而本节我们给出的替代方式是一对程序,它们分别把合法的 C 语言转换成文字描述,以及把文字描述反向转换成 C 语言。文字描述是从左往右读的。
第一个程序叫 dcl, 更复杂一些。它将 C 语言声明转换成【英文】文字描述,如下:
char **argv:
argv: pointer to pointer to char (指向char指针的指针)
int (*daytab)[13]
daytab: pointer to array[13] of int (指向有13个元素的整数数组的指针)
int *daytab[13]
daytab: arrar[13] of pointer to int (由13个整数指针组成的数组)
void *comp()
comp: function returning pointer to void (返回 void * 指针的函数)
void (*comp)()
comp: pointer to function returning void (指向返回 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 是一个 direct-dcl, 前面可能有 * 号。而 direct-dcl 可能是一个名称;或者是用圆括号括起来的 dcl;或是 direct-dcl 后面跟着一对圆括号;或是 direct-dcl 后面跟着一对方括号,其中的大小是可选的。
这个语法可以用来解析声明。例如下面这个声明
(*pfa[])()
pfa 被识别为一个名称,因此是 direct-dcl。然后 pfa[ ] 也是一个 direct-dcl。然后 *pfa[] 被识别为dcl,因此 (*pfa[]) 是 direct-dcl。然后 (*pfa[]) ( ) 是一个 direct-dcl,因此也是 dcl。也可以用如下的解析树来表示这个解析(其中 direct-dcl 简写为 dir-dcl):
dcl 程序的核心是一对根据这个语法来解析声明的函数,dcl 和 dirdcl。由于语法是递归定义的,当这两个函数识别出声明中的各个部分时,它们互相递归调用;程序被称为递归下降解析器。
/* dcl: 解析声明符 */
void dcl(void)
{
int ns;
for (ns = 0; gettoken() == '*';) /* 计算*号个数 */
ns++;
dirdcl();
while (ns-- > 0)
strcat(out, " pointer to");
}
/* dirdcl: 解析直接声明符 */
void dirdcl(void)
{
int type;
if (tokentype == '(') { /* ( dcl ) */
dcl();
if (toketype != ')')
printf("error: missing )\n");
} else if (tokentype == 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; /* 最后一个token的类型 */
char token[MAXTOKEN]; /* 最后一个token的字符串 */
char name[MAXTOKEN]; /* 标识符名称 */
char datatype[MAXTOKEN]; /* 数据类型为 char, int 等 */
char out[1000]; /* 输出字符串 */
main() /* 将声明转换为文字 */
{
while (gettoken() != EOF) { /* 行中的第一个token是数据类型 */
strcpy(datatype, token);
out[0] = '\0';
dcl(); /* 解析行的剩余部分 */
if (tokentype != '\n')
printf("syntax error\n");
printf("%s: %s %s\n", name, out, datatype);
}
return 0;
}
gettoken 函数跳过空白和制表符,然后找到输入中的下一个token;“token”是一个名称,或是一对圆括号,或是一对可能包含数字的中括号,以及任意单个字符。
int gettoken(void) /* 返回下一个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 = PARAENS;
} 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 在第四章中描述过。
反方向的处理会更简单,特别是如果我们不在乎生成了多余括号时。如 “x is a function returning an array of pointer to functions returning char” 这样的文字描述,在输入中表示为
x () * [] * () char
会被程序 undcl 转换为
char (*(*x())[])()
简化的输入语法让我们可以重用 gettoken 函数。 undcl 也使用了 dcl 使用的外部变量。
/* undcl:将文字描述转换为声明 */
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);
printf("%s\n", out);
}
return 0;
}
练习5-18、使 dcl 从输入错误中恢复。
练习5-19、修改 undcl 使其不产生多余的括号。
练习5-20、扩展 dcl ,使其能处理的声明可以包含函数参数类型,包含如 const 之类修饰符等。
(第五章完)