C语言声明的神奇解码环

      在《C专家编程》的第65页,有一个用于解析C语言声明的流程图——C语言声明的神奇解码环,相信看过此书的读者都会对此有印象。不过我觉得这张图画的还不是太完善,在该书66页用它来解析C语言声明的步骤的表格也不是太严格。下面我将我认为的比较完善的解码环和相应的分析步骤陈述如下,不妥之处请读者指出。

                           

                                                                      图1  C语言声明的神奇解码环

       (说明:图1中,中轴箭头左侧符号即为声明中已匹配部分左侧的符号,中轴箭头右侧的符号即为声明中已匹配部分右侧的符号,明白这一点有助于声明的分析流程)

        让我们以书上的声明  “char * const * (*next)(); ”为例来分析该声明表达的含义。

        在分析这个声明时,需要逐渐把已经处理过的片段“去掉”,这样便能知道还需要分析多少内容。处理过程见下表,在每一步骤里,所处理的那部分声明用红色字体表示。从第一步开始,我们将依次进行这些步骤。

剩余的声明

(从最左边的标识符开始)

所采取的下一步骤

结果

char * const * (*next)();

第1步

表示“next 是…”

char * const * (*      )();

第2步

不匹配,转到下一步

char * const * (*      )();

第3步

不匹配,转到下一步

char * const * (*      )();

第4步

不匹配,转到下一步

char * const * (*      )();

第5步

与星号匹配,表示“指向…的指针”,转第4步

char * const * (       )();

第4步

“(”和“)”匹配,转到第2步

char * const *          ();

第2步

不匹配,转到下一步

char * const *          ();

第3步

表示“返回…的函数”,转到下一步

char * const *            ;

第4步

不匹配,转到下一步

char * const *            ;

第5步

表示“指向…的指针”

char * const               ;

第5步

表示“只读的…”

char *                        ;

第5步

表示“指向…的指针”,转到第4步

char                          ;

第4步

不匹配,转到下一步

char                          ;

第5步

不匹配,转到下一步

char                          ;

第6步

表示“char”

       拼在一起读作:next是一个函数指针,该函数返回另一个指针,该指针指向一个只读的指向char的指针;

       换句话说:next是一个函数指针,它指向如下样子的函数,

                     1. 函数的返回值是一个指针(它其实是一个二级指针),

                     2. 该指针指向一个类型是char的常量指针(这里的常量意为只读的)。

 

        下面让我们来分析一个更复杂的例子 “char *(* c[10])(int **p);” ,也就是书上的第二个例子。

剩余的声明

(从最左边的标识符开始)

所采取的下一步骤

结果

char *(* c[10])(int **p);

第1步

表示“c 是…”

char *(*   [10])(int **p);

第2步

与“[10]”匹配,表示“c 是有10个元素的数组”,转到下一步(注意,这里匹配了“[10]”,所以下一步是第4步

char *(*        )(int **p);

第4步

不匹配,转到下一步

char *(*        )(int **p);

第5步

与星号匹配,表示“指向…的指针”,转第4步

char *(         )(int **p);

第4步

“(”和“)”匹配,转到第2步

char *           (int **p);

第2步

不匹配,转到下一步

char *           (int **p);

第3步

表示“返回…的函数,且函数参数为整形二级指针”,转到下一步

char *                       ;

第4步

不匹配,转到下一步

char *                       ;

第5步

与星号匹配,表示“指向…的指针”,转第4步

char                          ;

第4步

不匹配,转到下一步

char                          ;

第5步

不匹配,转到下一步

char                          ;

第6步

表示“char”

        拼在一起读作:c是一个数组[0..9],它的元素类型是函数指针,其所指向的函数的返回值是一个指向char的指针,并且,在数组中被函数指针所指向的函数都把一个指向int型指针的指针作为它们的唯一参数;

       换句话说:c是一个具有是10个元素的数组,它的每一个元素都是一个函数指针,这些函数指针指向如下样子的函数:

                     1. 函数的返回值是一个指向char的指针,

                     2. 函数的参数是一个指向int型指针的指针(int型二级指针)。

 

      在上面的图中,箭头绕来绕去的看起来有点复杂。我们现在再对上图稍作修改,主要目的有两个:1.让这个声明解析的流程图看起来更直观,方便我们进行声明解析;2.让这个流程图与书中给出的代码实现相一致。

我们下面再用这个修改过的流程图对之前的两个声明进行解析。

char * const * (*next)();

剩余的声明

(从最左边的标识符开始)

所采取的下一步骤

结果

char * const * (*next)();

第1步

表示“next 是…”

char * const * (*       )();

第2步

不匹配,转到下一步

char * const * (*       )();

第3步

不匹配,转到下一步

char * const * (*      )();

第4步

与星号匹配,表示“指向…的指针”,转到下一步

char * const * (        )();

第5步

“(”和“)”匹配,转到第2步

char * const *           ();

第2步

不匹配,转到下一步

char * const *           ();

第3步

表示“返回…的函数”,转到下一步

char * const *             ;

第4步

表示“指向…的指针”

char * const                ;

第4步

表示“只读的…”

char *                         ;

第4步

表示“指向…的指针”,转到下一步

char                            ;

第5步

不匹配,转到下一步

char                            ;

第6步

表示“char”

char *(* c[10])(int **p);

剩余的声明

(从最左边的标识符开始)

所采取的下一步骤

结果

char *(* c[10])(int **p);

第1步

表示“c 是…”

char *(*   [10])(int **p);

第2步

与“[10]”匹配,表示“c 是有10个元素的数组”,转到下一步(注意,这里匹配了“[10]”,所以下一步是第4步

char *(*        )(int **p);

第4步

与星号匹配,表示“指向…的指针”,转到下一步

char *(         )(int **p);

第5步

“(”和“)”匹配,转到第2步

char *           (int **p);

第2步

不匹配,转到下一步

char *           (int **p);

第3步

表示“返回…的函数,且函数参数为整形二级指针”,转到下一步

char *                      ;

第4步

与星号匹配,表示“指向…的指针”,转到下一步

char                        ;

第5步

不匹配,转到下一步

char                        ;

第6步

表示“char”

        从以上分析过程可以看出,采用修改过后的声明解码环,对“char * const * (*next)()”的分析总共用了12步(之前15步),对“char *(* c[10])(int **p)”的分析总共用了9步(之前12步),这样对C语言声明的解析过程就得到了简化,解码环看起来也更直观。一切看起来都很美好!

 

        下面我们再来看看具体的代码实现。从具体的代码实现上可以看出修改后的解码环是与其保持一致的。

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MAXTOKENS 100
#define MAXTOKENLEN 64

enum type_tag{ IDENTIFIER, QUALIFIER, TYPE };

struct token
{
    char type;
    char string[MAXTOKENLEN];
};

int top = -1;
struct token stack[MAXTOKENS];
struct token this;

#define pop stack[top--]
#define push(s) stack[++top] = s

enum type_tag classify_string(void)//推断标识符的类型
{
    char *s = this.string;
    if( !strcmp(s, "const") )
    {
        strcpy(s, "ready-only");//把const翻译成read-only
        return QUALIFIER;
    }
    if( !strcmp(s, "volatile") )      return QUALIFIER;
    if( !strcmp(s, "void") )    return TYPE;
    if( !strcmp(s, "char") )      return TYPE;
    if( !strcmp(s, "signde") )      return TYPE;
    if( !strcmp(s, "unsigned") )      return TYPE;
    if( !strcmp(s, "short") )      return TYPE;
    if( !strcmp(s, "int") )      return TYPE;
    if( !strcmp(s, "long") )      return TYPE;
    if( !strcmp(s, "float") )      return TYPE;
    if( !strcmp(s, "double") )      return TYPE;
    if( !strcmp(s, "struct") )      return TYPE;
    if( !strcmp(s, "union") )      return TYPE;
    if( !strcmp(s, "enum") )      return TYPE;
    return IDENTIFIER;
}

void gettoken(void)// 读取下一个标记到“this”
{
    char *p = this.string;

    while( (*p = getchar()) == ' ' );// 略过空白字符

    if( isalnum(*p) ) //原型:extern int isalnum(int c); 说明:当c为数字0-9或字母a-z及A-Z时,返回非零值,否则返回零。
    {
        while( isalnum( *++p = getchar() ) );
        ungetc(*p, stdin);
        *p = '\0';
        this.type = classify_string();
        return;
    }

    if( *p == '*')
    {
        strcpy(this.string, "pointer to");
        this.type = '*';
        return ;
    }
    this.string[1] = '\0';//这里,this.string[0]可能是‘(’,‘)’,‘[’,‘]’,‘,’
    this.type = *p;
    return;
}
//理解所有分析过程的代码段
void read_to_first_identifer()//从左到右,读取到第一个标识符,将该标识符输出,并将该标识符后面的一个标记读入到this。
{
    gettoken();
    while( this.type != IDENTIFIER )
    {
        push(this);
        gettoken();
    }
    printf("%s is ",this.string);
    gettoken();
}

void deal_with_arrays()
{
    while( this.type == '[' )//这里while循环是为了处理多维数组的情况
    {
        printf("array ");
        gettoken(); // 数字或者 ']'
        if( isdigit(this.string[0]) )
        {
            printf("0..%d ",atoi(this.string)-1);//这里减1,是因为10个元素的数组,下标只有0到9
            gettoken();//读取 ']'
        }
        gettoken();   //读取 ']'之后的再一个标记
        printf("of ");
    }
}

void deal_with_function_args()
{
    while( this.type != ')' )
    {
        gettoken();
    }
    gettoken();
    printf("function returning ");
}

void deal_with_pointers()
{
    while( stack[top].type == '*' )
    {
        printf("%s ", pop.string);//打印字符串“pointer to”
    }
}

void deal_with_declarator()// 处理标识符以后的可能存在的函数/数组
{
    switch(this.type)
    {
        case '[' :deal_with_arrays(); break;
        case '(' :deal_with_function_args();
    }

    deal_with_pointers();

    // 处理读入到标识符之前压入到栈中的符号
    while(top >= 0)
    {
        if(stack[top].type == '(')
        {
            pop;
            gettoken(); //读取')'之后的符号,这个注释很重要,因为从理论上来说,对于一个格式正确的声明,当stack[top].type = '('时,这时候this中保存的应当就是与之匹配的')',所以再调用gettoken()后,this中保存的就是')'之后的符号了。
            deal_with_declarator(); //这里的递归调用需要注意:变量top是一个全局变量,而非局部变量,所以“内层函数”执行时对top的修改会影响到“外层函数”;另外,内外层函数处理的是同一个stack中内容
        }
        else
        {
            printf("%s ",pop.string);
        }
    }
}

int main(void)
{
    // 将标记压入堆栈中,直到遇见标识符
    read_to_first_identifer();
    deal_with_declarator();
    printf("\n"); 
    return 0;
}

        以上程序中难以理解的部分已经用注释给出详细说明。至此,对C语言声明的解码环以及利用解码环来分析C语言声明的过程描述完毕,希望对读者理解C语言晦涩的声明方式有所帮助,不妥之处还请读者指出。

(All Rights Reserved)

posted @ 2016-03-06 13:24  colabean  阅读(389)  评论(0编辑  收藏  举报