一、写在前面

K&R曾经在书中承认,"C语言声明的语法有时会带来严重的问题。"。由于历史原因(BCPL语言只有唯一一个类型——二进制字),C语言声明的语法在各种合理的组合下会变得晦涩难懂。不过在15级的优先级规则加持下,C语言的声明仍然有迹可循。这篇文章讲解了一个通常取名为"cdecl"(不同于函数调用约定)的小型程序,该程序常用来解析C语言的声明。本程序的基始版本来源于《C专家编程》p75,约140行代码。

博主在这个程序的基础上,增加了两个模块的功能:

1、struct/enum/union关键字后标签变量名的甄别

有如下声明:struct student a; 在这个声明中student是作为struct后可选的"结构标签"出现的,a才是变量名称。

2、函数参数的处理

源程序略过了函数参数处理的模块,在此,我们加入了此功能,尽管有些简化。

二、声明的组成部分

通常情况下来讲,一个C语言声明由三部分组成:类型说明符+声明名称(declarator)+分号,如int a;

三、优先级规则

1、声明从它的名字开始读取,随后按照优先级顺序依次读取。

2、优先级从高到低依次是:

2.1、声明中被括号括起来的那部分

2,2、后缀操作符:

符号 () 表示这是一个函数

符号 [] 表示这是一个数组

2.3、前缀操作符:*代表"指向...的指针"

3、如果const/volatile关键字后面紧跟类型说明符(如int),那么该关键字作用于类型说明符。在其他情况下,const/volatile关键字作用于它左边紧邻的指针星号。

因此运用该规则分析如下声明:char *(*c[10])();

    第一步:找到变量名c

第二步:处理c后的[10],表示"c是一个有10个元素的数组"

第三步:处理c前的*,表示"数组元素为指针"

第四步:处理c所在括号后的括号,表示"数组的元素类型是函数指针"

第五步:处理(*c[10])前的星号,表示"数组元素指向的函数的返回值是一个指针"

第六步:处理char,表示"数组元素指向的函数的返回值是一个指向char的指针"

综上,该声明表示:C是一个有10个元素的数组,数组元素类型是函数指针,其所指向的函数的返回值是一个指向char的指针。

四、程序执行流程

由于C语言声明并不可以从左往右直接解析,所以我们需要一个栈结构来保存在读取到声明名称前的所有字段,以便在读取到id后再分析。

  1. struct token{  
  2.     char type;  
  3.     char string[MAXTOKENLEN];  
  4. };  
  5. struct token stack[MAXTOKENS];  

    将所有字段分为三类:名称、类型以及限定词,使用枚举类型,使之与char type对应。

  6. enum type_tag {  
  7.     IDENTIFIER,QUALIFIER,TYPE  
  8. };  

    主函数有两大功能,一是找到identifier,二是处理剩下的声明。

  9. int main (void)  
  10. {  
  11.     read_to_first_identifier();   
  12.     deal_with_declarator();   
  13.         
  14.     return 0;  
  15. }  

    第一个函数从左往右读入输入数据,一个读取一个字段(声明的基本单位),若字段不是id(标识符),则将其压入栈中,再读取下一个字段,直到读取到字段,该阶段任务结束。

    第二个函数在得到id后开始工作。根据语法规则,先读取id后的字符,判断其为数组还是函数。在处理完id后的字段后,再依次出栈解析前面的声明。

五、各模块代码

5.1、读取标识符:read_to_first_identifier();

使用一个循环,每次读取一个字段,并判断其是否为标识符,是,则退出,并输出。对于正在读取的标识符,使用一个全局变量struct token thistoken存储,在处理完该字段后,若其不为标识符,则压入栈中。

  1. void read_to_first_identifier()  
  2. {  
  3.     gettoken();  
  4.     while(thistoken.type != IDENTIFIER)  
  5.     {  
  6.         push(thistoken);  
  7.         gettoken();  
  8.     }  
  9.     printf("%s is ",thistoken.string);  
  10.     gettoken();  
  11. }  

5.2、读取各字段:gettoken();

我们假设各个含有英文字母的字段(如类型说明符、标识符等)都以空格隔开,因此我们可以从我们读取到的第一个非空字符开始,判断它的类型。标识符前的符号有一下几种:说明符、指针(*)。所以我们将其单独处理。

  1. void gettoken()  
  2. {  
  3.     char *p = thistoken.string;  
  4.         
  5.     while((*p = getchar()) == ' ');  
  6.         
  7.     if(isalnum(*p))  
  8.     {  
  9.         while(isalnum(*++p = getchar()));  
  10.         ungetc(*p,stdin);  
  11.         *p = '\0';  
  12.         thistoken.type = classify_string();  
  13.         return ;  
  14.     }  
  15.         
  16.     if(*p == '*')  
  17.     {  
  18.         strcpy(thistoken.string,"pointer to");  
  19.         thistoken.type = '*';  
  20.         return ;  
  21.     }  
  22.     thistoken.string[1] = '\0';  
  23.     thistoken.type = *p;  
  24.     return ;  
  25. }  

对于标识符及声明符,我们在读取完一个字段后就判断其类型。对于'*'或其他符号('(''['等),则直接用符号本身作为其类型。

5.3、解析字段类型:classify_string ();

在我们提取到完整的英文/数字字段后,通过该函数来推断其类型。通过strcmp()函数,将其与各个类型说明符对比,如果一样,则返回类型说明符,如type/qualifier。与因为用strcmp()函数来比较字符串时,字符串相等,函数返回值为0。为了在相等时得到我们想要的真值,就需要对其进行取反。除了用"!"外,用宏来解决更方便。

  1. #define STRCMP(a,R,b) (strcmp(a,b) R 0)  

因此,字符串的比较就成了如下形式:

  1. if(STRCMP(s,==,"void"))  
  2.     return TYPE;  

如果读取到的字段并非限定符或者说明符,则认为其为标识符。

  1. enum type_tag classify_string()  
  2. {  
  3.     char *s = thistoken.string;  
  4.     if(STRCMP(s,==,"const"))  
  5.     {  
  6.         strcpy(s,"read-only");  
  7.         return QUALIFIER;  
  8.     }  
  9.     if(STRCMP(s,==,"volatile"))  
  10.         return QUALIFIER;  
  11.     if(STRCMP(s,==,"void"))  
  12.         return TYPE;  
  13.     if(STRCMP(s,==,"char"))  
  14.         return TYPE;  
  15.     if(STRCMP(s,==,"singed"))  
  16.         return TYPE;  
  17.     if(STRCMP(s,==,"unsinged"))  
  18.         return TYPE;  
  19.     if(STRCMP(s,==,"short"))  
  20.         return TYPE;  
  21.     if(STRCMP(s,==,"int"))  
  22.         return TYPE;  
  23.     if(STRCMP(s,==,"long"))  
  24.         return TYPE;  
  25.     if(STRCMP(s,==,"float"))  
  26.         return TYPE;  
  27.     if(STRCMP(s,==,"double"))  
  28.         return TYPE;  
  29.     if(STRCMP(s,==,"struct"))  
  30.     {  
  31.         check_type_or_id(s);  
  32.         return TYPE;  
  33.     }  
  34.     if(STRCMP(s,==,"union"))  
  35.     {  
  36.         check_type_or_id(s);  
  37.         return TYPE;  
  38.     }  
  39.     if(STRCMP(s,==,"enum"))  
  40.     {  
  41.         check_type_or_id(s);  
  42.         return TYPE;  
  43.     }  
  44.     return IDENTIFIER;  
  45. }  

5.4、解析字段类型:check_type_or_id();

对于类型struct/type/enum,在声明该类型变量时,类型后的字段极有可能是该关键字后可选的"结构标签"。如声明struct student xxx;,student是作为一个结构标签存在。该声明与struct student {内容…}xxx;一致。所以在判断student时,需要看它后面字段的类型。如果struct后两个字段都为标识符,则最后一个标识符才是真的标识符,类型struct/type/enum后的字段则是该类型的另一个名字,如:xxx是一个叫student的结构体。

在该模块的实现上,则是在读取到结构struct/type/enum时,再读取其后的两个标签,再判断,并将真正的标识符及其后的内容返回到输入流中。

  1. void check_type_or_id(char *s)  
  2. {  
  3.     char temp[MAXTOKENLEN] = {'\0'};  
  4.         
  5.     struct token temp_struct_one = thistoken;  
  6.         
  7.     gettoken();  
  8.     struct token temp_struct = thistoken;  
  9.         
  10.     gettoken();  
  11.     struct token temp_struct3 = thistoken;  
  12.                 
  13.     if(thistoken.type==IDENTIFIER)  
  14.     {  
  15.         strcat(temp,temp_struct_one.string);  
  16.         strcat(temp," called ");  
  17.         strcat(temp,temp_struct.string);  
  18.         strcpy(s,temp);  
  19.         thistoken = temp_struct3;  
  20.         strcpy(temp_struct_one.string,temp);  
  21.     }  
  22.     else  
  23.     {  
  24.         thistoken = temp_struct;   
  25.         for(int i = strlen(temp_struct3.string)-1;i>=0;i--)  
  26.         {  
  27.             ungetc(temp_struct3.string[i],stdin);  
  28.         }  
  29.     }  
  30.         
  31.     if(thistoken.type>=0 && thistoken.type<=2)  
  32.     {  
  33.         for(int i = strlen(thistoken.string)-1;i>=0;i--)  
  34.         {  
  35.             ungetc(thistoken.string[i],stdin);  
  36.         }  
  37.     }  
  38.     thistoken = temp_struct_one;  
  39. }  

5.5、声明的处理:deal_with_declarator();

在确定了标识符之后,我们就可以处理各种声明、修饰符了。依据优先级规则,我们先需要观察标识符后的符号,以确定其是否是数组/函数;其后还需要处理指针,最后再处理先前被压栈的符号。

在开始该阶段的处理之前,我们观察read_to_first_identifier()函数,在该函数的最后一行,我们确定了标识符后,有进行了一次gettoken(),这次调用即将标识符后的符号读入,因此现在在函数开头我们就可以使用switch()直接选择要处理的情况。

  1. void deal_with_declarator()  
  2. {  
  3.     switch(thistoken.type)  
  4.     {  
  5.         case '[':  
  6.             deal_with_arrays();  
  7.             break;  
  8.         case '(':  
  9.             deal_with_function_args();  
  10.             break;  
  11.     }  
  12.         
  13.     deal_with_pointers();  
  14.         
  15.     while(top >= 0)  
  16.     {  
  17.         if(stack[top].type == '(')  
  18.         {  
  19.             pop;  
  20.             gettoken();  
  21.             deal_with_declarator();  
  22.         }  
  23.         else  
  24.         {  
  25.             printf("%s ",pop.string);  
  26.         }  
  27.     }  
  28. }  

5.6、函数参数的处理:deal_with_function_args();

在《C专家编程》中,没有对函数参数进行处理。在此,我加入了对参数的简单处理。简单处理也即,对于复杂声明的参数,并没有能正确的处理。在我写这个模块时,我有一种对整个程序重构的想法,即将声明的解析抽象成一个独立的函数,现在程序里全局变量对函数功能的拓展限制太大了。

该函数的流程则是,将括号内的字段全部读取并输出,遇到','重新读取输出。普通单一的类型说明符可直接输出(如 int a),而int *a;则无法如此简单处理。由于输出使用的是英语,所以该函数大部分的代码都是在处理不同参数时英语表述的语法问题,如但单参数的'parameter is'与多参数的'parameters are'等语法细节。处理粗糙,不看也罢。

  1. void deal_with_function_args()  
  2. {  
  3.     char str[MAXTOKENLEN] = {'\0'};  
  4.     char para[MAXTOKENLEN] = {'\0'};   
  5.     bool flag_no_para = true;  
  6.     bool para_is_one = true;  
  7.     
  8.     strcat(str,"function");  
  9.     
  10.     gettoken();  
  11.          
  12.     if(thistoken.type != ')')  
  13.     {  
  14.         strcat(str," whose parameter");  
  15.         flag_no_para = false;  
  16.     }     
  17.     while(thistoken.type != ')')  
  18.     {  
  19.         if(thistoken.string[0] == ',')  
  20.         {  
  21.             if(para_is_one == true)  
  22.             {  
  23.                 strcat(str,"s are");  
  24.                 para_is_one = false;  
  25.             }  
  26.                     
  27.             strcat(para," and");  
  28.         }  
  29.         else  
  30.         {  
  31.             strcat(para," ");  
  32.             strcat(para,thistoken.string);  
  33.         }     
  34.         gettoken();  
  35.     }  
  36.     if(para_is_one == true && flag_no_para== false)  
  37.     {  
  38.         strcat(str," is");  
  39.     }  
  40.     strcat(str,para);  
  41.     gettoken();  
  42.     if(flag_no_para == true)  
  43.     {  
  44.         strcat(str," returning ");  
  45.     }  
  46.     else  
  47.     {  
  48.         strcat(str,",it returns ");  
  49.     }  
  50.     printf("%s",str);   
  51. }  

六、一些声明的解析结果

七、写在后面

新增的功能并不尽如人意,不过也将这次的修改探索总结出来,以供后来者学习,希望后来者少踩一些坑,老老实实重构去哈哈哈。

最后….预祝新年快乐~

源码地址:C语言声明解析器修改版源码

[C语言]声明解析器cdecl修改版的更多相关文章

  1. C语言声明解析方法

    1.C语言声明的单独语法成份     声明器是C语言声明的非常重要成份,他是所有声明的核心内容,简单的说:声明器就是标识符以及与它组合在一起的任何指针.函数括号.数组下表等,为了方便起见这里进行分类表 ...

  2. golang开发:类库篇(四)配置文件解析器goconfig的使用

    为什么要使用goconfig解析配置文件 目前各语言框架对配置文件书写基本都差不多,基本都是首先配置一些基础变量,基本变量里面有环境的配置,然后通过环境变量去获取该环境下的变量.例如,生产环境跟测试环 ...

  3. c语言复杂声明解析

    这是个好东西,接触c语言好几年了,第一次看到这东西,惊喜万分. 先提供个分析案例,以后看方便 vector <int> * (*seq_array[]) (int )={func1,fun ...

  4. Scala词法文法解析器 (二)分析C++类的声明

    最近一直在学习Scala语言,偶然发现其Parser模块功能强大,乃为BNF而设计.啥是BNF,读大学的时候在课本上见过,那时候只觉得这个东西太深奥.没想到所有的计算机语言都是基于BNF而定义的一套规 ...

  5. atitit.java解析sql语言解析器解释器的实现

    atitit.java解析sql语言解析器解释器的实现 1. 解析sql的本质:实现一个4gl dsl编程语言的编译器 1 2. 解析sql的主要的流程,词法分析,而后进行语法分析,语义分析,构建sq ...

  6. C# 语言的两个html解析器

    基于C# 语言的两个html解析器   基于C# 语言的两个html解析器 1)Html Agility Pack http://nsoup.codeplex.com/ 代码段示例: HtmlDocu ...

  7. 基于C# 语言的两个html解析器

    基于C# 语言的两个html解析器 1)Html Agility Pack http://nsoup.codeplex.com/ 代码段示例: HtmlDocument doc = new HtmlD ...

  8. 手写token解析器、语法解析器、LLVM IR生成器(GO语言)

    最近开始尝试用go写点东西,正好在看LLVM的资料,就写了点相关的内容 - 前端解析器+中间代码生成(本地代码的汇编.执行则靠LLVM工具链完成) https://github.com/daibinh ...

  9. 【P4语言学习】Parser解析器

    参考文章:王垠:谈谈Parser 簡單介紹 P4 語言(一)- Parser 什么是Parser 传统的parser,一般出现在编译器和编译原理课程中,援引<谈谈Parser>的定义: 首 ...

随机推荐

  1. hdu 4994 前后有序Nim游戏

    http://acm.hdu.edu.cn/showproblem.php?pid=4994 Nim游戏变成从前往后有序的,谁是winner? 如果当前堆数目为1,玩家没有选择,只能取走.遇到到不为1 ...

  2. Flash CC2015软件安装教程

    FLCC2015/64位下载地址: 链接:https://pan.baidu.com/s/1c1WoTTu 密码:k4hn 软件介绍: Flash是一种动画创作与应用程序开发于一身的创作软件.Flas ...

  3. sqlserver 实现数据变动触发信息

    1.建立存储过程,功能是动态写入文件中信息,可以在触发器或存储过程调用. SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO create proc [d ...

  4. FreeNas FTP配置

    FTP服务器与客户端 因为拥有强大WebGUI管理界面,在FreeNAS中配置FTP服务变得非常简单.如果你是第一次接触FTP这一概念,那么首先要明白两个核心的概念. FTP服务器:你可以把它想象成一 ...

  5. Tasks遇到的一些坑,关于在子线程中对线程权限认证。

    一般情况下,不应该在执行多线程认证的时候对其子线程进行身份认证,如:A线程的子线程B和子线程C. 当使用 Parallel.ForEach方法时,只有自身线程能够拥有相对应的权限,其子线程权限则为NU ...

  6. NetCore偶尔有用篇:NetCore项目发布为Nuget包

    一.简介 1.nuget大家已经不陌生. 2.netcore默认引用便是nuget,并处理了嵌套关系. 3.netcore已经支持直接编译生成nuget包. 4.本文介绍如何把自己建立的项目发布为nu ...

  7. 在Windows安装Reids 详解

    今天安装了redis,记录点经验 因为Redis项目没有正式支持Windows. 但Microsoft开发和维护一个针对Windows 64版的redis. 下载地址在微软的GitHub上,地址:ht ...

  8. Unity里vertexShader里压扁模型来实现比较low的阴影

    只有阴影pass,请自行合并,需要指定高度,忽略深度检测,需要控制好排序,或者去掉忽略,视情况而定,最后我觉得还是shadowmap好 Shader "Custom/MeshShadow&q ...

  9. 文本比较算法Ⅱ——Needleman/Wunsch算法的C++实现【求最长公共子串(不需要连续)】

    算法见:http://www.cnblogs.com/grenet/archive/2010/06/03/1750454.html 求最长公共子串(不需要连续) #include <stdio. ...

  10. day74天中间件介绍

    一. importlib settings 执行结果: 两个process_request  process_response按照注册顺序的倒叙进行执行 PROCESS_VIEW  Process_v ...