从头学C(54)复杂声明

0

C 语言常常因为声明的语法问题而饱受批评,尤其是当涉及到函数指针的语法。尽管 C 语言的语法努力使声明和使用保持一致,但很多复杂的声明,还是会让人一头雾水。原因在于,C语言的声明不能单纯地从左至右阅读,而且其中使用了太多的圆括号。

第五章 指针与数组 >> 5.12 复杂声明

先从两个声明的对比来看:

int *f();       /* f: function returning pointer to int */
int (*pf)();    /* pf: pointer to function returning int */

前者是一个函数,返回值是一个指向 int 类型数据的指针;后者是一个函数指针,返回值是 int 类型数据,其中由于 * 运算符的优先级低于圆括号 ( ) ,所以必须使用圆括号以保证正确的结合顺序。

虽然实际中很少会用到过于复杂的声明,但是懂得如何去理解、使用这些复杂声明还是非常重要的。通过 typedef 进行简单的步骤合成,是一种办法,这个我们在后面的章节会学习到。但现在,我们通过一个 dcl 程序来将正确的 C 语言声明转换为文字描述。

比如,该程序可以将以下声明:

char **argv
int (*daytab)[13]
int *daytab[13]
void *comp()
void (*comp)()
char (*(*x())[])()
char (*(*x[3])())[5]

转换为对应的文字描述:

argv: pointer to pointer to char
daytab: pointer to array[13] of int
daytab: array[13] of pointer to int
comp: function returning pointer to void
comp: pointer to function returning void
x: functiong returning pointer to array[] of pointer to function returning char
x: array[3] of pointer to function returning pointer to array[5] of char

程序 dcl 是基于声明符的语法编写的(需要参考原书《The C Programming Language》的附录 A 的第 8.5 节说明)。语法形式是:

declarator:
        pointer opt direct-declarator
direct-declarator:
        identifier
        (declarator)
        direct-declarator[constant-expression opt]
        direct-declarator(parameter-type-list)
        direct-declarator(identifier-list opt)
pointer:
        * type-qualifier-list opt
        * type-qualifier-list opt pointer
type-qualifier-list:
        type-qualifier
        type-qualifier-list type-qualifier

备注:opt 表示位于其前面的标识符是可选的意思。

简而言之,声明符 declarator 就是前面可能带有多个 * 号的 direct-declarator。而 direct-declarator 可以是 identifier 名字、由一对圆括号括起来的 declarator、后面跟有一对圆括号的 direct-declarator 、后面根由用方括号括起来的表示可选长度的 direct-declarator。

利用该语法来对 C 语言的声明进行分析。例如:

(*pfa[])()

一步步分析下来:

  1. pfa 将被识别为是一个 identifier ,从而被认为是一个 direct-declarator。
  2. 于是 pfa[ ] 也是一个 direct-declarator。
  3. 接着, *pfa[ ] 被识别为一个 dcl,因此判定 (*pfa[ ]) 是一个 direct-declarator。
  4. 最后,(*pfa[ ])( ) 会被识别为一个 direct-declarator,因此也是一个 declarator。

下图是对应的语法分析树(其中 declarator 缩写为 dcl,direct-declarator 缩写为 dir-dcl):

C语言_复杂声明

程序 dcl 的完整代码如下:

#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];

/* convert declaration to words */
int main()
{
        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;
}

/* 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");
                }
}

/* return next token */
int gettoken(void)
{
        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;
}

备注:该程序旨在说明问题,并未做到尽善尽美,所以对 dcl 有很多限制,比如只能处理类似于 char 或 int 这样的简单数据类型,而无法处理函数中的形式参数类型或类似于 const 这样的限定符。它也不能处理带有不必要空格的情况。如果感兴趣,大家可以再完善一下。

程序最核心的两个函数是:dcl 和 dirdcl ,它们根据声明符的语法对声明进行分析。由于语法本身也是递归定义的,所以在识别一个声明的组成部分时,可以看到这两个函数时相互递归调用的。我们称该程序是一个递归下降语法分析程序

函数 gettoken 用来略过空格和制表符,以查找下一个记号(token)。该记号可以是一个名字、一对圆括号、包含一个数字的一对方括号、不包含数字的一对方括号,或是其他任何单个字符。

函数 getch 和 ungetch 可以参考《从头学C(34)外部变量》中的 getch_ungetch.c 文件。

既然有了可以将 C 语言声明转换为文字描述的 dcl 程序,那么相应的,我们再来看一个将文字描述转换为对应 C 语言声明的 undcl 程序。

为了简化程序的输入,我们将“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: convert word descriptions to declarations */
int 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;
}

至此,我们第五章关于指针和数组的学习就告一段落了,更多的细节还需我们在实际编程中慢慢掌握,下一节我们将开始学习第六章结构体。

Leave A Reply