编译原理解释器(一)C语言词法分析器的实现

辣鸡的我终于在一个已经保研的小哥哥(萌似泰迪)的帮助下完成了解释器!!(VS2013)

分为3步:词法分析器、语法分析器、语义分析器

代码大部分出自《编译原理基础-习题与上机解答》(西安电子科技大学出版社)中的附录

下面会上所有代码附带(超级)大量详细注释和理解,以及很多处理细节。因为在这些在高手看来顺理成章的过程才是新手很大的障碍。

step 1

安装Virsual Stidio 2013

经过我的实践和另一个小哥哥的经验:windos7只能安装vs2013版的,否则就会出现下面这种2015版装了3个小时进度条还没前进的情况

  

不要装在C盘,这样如果系统崩了,C盘会全丢失,而且放进C盘电脑会运行变慢。

step 2

上代码之前,先来说说词法分析器。

“sacanner”是词法分析器,输入流是序列,输出流是“字典”。

我们需要

1、设计记号:词法分析器读取一个序列并根据构词规则把序列转化为记号流

2、定义一个字典:把所有符合一个模式的保留字、常量名、参数名、函数名等放进字典。字典是个数组,其元素的类型和记号的类型相同

3、设计程序的结构,具体见下面的代码

step3(重头戏)

新建一个词法分析器的项目

把已经编写好的代码扔进去:scanner.h scanner.c scannermain.c

 1 //-----------------------scanner.h--------------------
 2 #pragma once
 3 //#ifndef SCANNER_H
 4 //#define SCANNER_H
 5 #define _CRT_SECURE_NO_WARNINGS
 6 
 7 #include<stdio.h>
 8 #include<string.h>
 9 #include<stdlib.h>
10 #include<ctype.h>
11 #include<stdarg.h>
12 #include<math.h>
13 
14 enum Token_Type//枚举记号的类别
15 {
16     ORIGIN,SCALE,ROT,IS,TO,STEP,DRAW,FOR,FROM,//保留字
17     T,//参数
18     SEMICO,L_BRACKET, R_BRACKET,COMMA,//分隔符
19     PLUS,MINUS,MUL,DIV,POWER,//运算符
20     FUNC,//函数
21     CONST_ID,//常数
22     NONTOKEN,//空记号
23     ERRTOKEN//出错记号
24 };
25 
26 typedef double(*MathFuncPtr) (double);
27 
28 struct Token//记号的数据结构  记号由类别和属性组成
29 {
30     Token_Type type;//记号的类别 
31     char *lexeme;//属性,原始输入的字符串    是个字符指针,当需要记号的字符串时,就会引用这个指针,但是字符串保留在TokenBuffer中,所以要指向TokenBuffer
32     double value;                                        //为常数设置,是常数的值
33     double(*FuncPtr)(double);                            //为函数设置,是函数的指针
34 };
35 //正规式个数越少越利于程序编写,所以把相同模式的记号共用一个正规式描述,要设计出一个预定义的符号表(就是一个数组),进行区分~
36 static Token TokenTab[]=//符号表(字典):数组元素的类型于记号的类型相同
37 {//当识别出一个ID时,通过此表来确认具体是哪个记号
38     { CONST_ID,    "PI",        3.1415926,    NULL },
39     { CONST_ID,    "E",        2.71828,    NULL },
40     { T,        "T",        0.0,        NULL },
41     { FUNC,        "SIN",        0.0,        sin    },
42     { FUNC,        "COS",        0.0,        cos },
43     { FUNC,        "TAN",        0.0,        tan },
44     { FUNC,        "LN",        0.0,        log },
45     { FUNC,        "EXP",        0.0,        exp },
46     { FUNC,        "SQRT",        0.0,        sqrt },
47     { ORIGIN,    "ORIGIN",    0.0,        NULL },
48     { SCALE,    "SCALE",    0.0,        NULL },
49     { ROT,        "ROT",        0.0,        NULL },
50     { IS,        "IS",        0.0,        NULL },
51     { FOR,        "FOR",        0.0,        NULL },
52     { FROM,        "FROM",        0.0,        NULL },
53     { TO,        "TO",        0.0,        NULL },
54     { STEP,        "STEP",        0.0,        NULL },
55     { DRAW,        "DRAW"    ,    0.0,        NULL }
56 };
57 
58 extern unsigned int LineNo;                                //跟踪记好所在源文件行号
59 extern int InitScanner(const char*);                    //初始化词法分析器
60 extern Token GetToken(void);                            //获取记号函数
61 extern void CloseScanner(void);                            //关闭词法分析器
62 
63 //#endif
  1 #include"scanner.h"
  2 #ifndef MSCANNER_H
  3 #define MSCANNER_H
  4 #define TOKEN_LEN 100//设置一个字符缓冲区,这是他的大小用来保留记号的字符串
  5 unsigned int LineNo;//记录字符所在行的行号-》词法分析器对每个记号的字符串进行分析时必须记住该字符串在源程序的位置
  6 static FILE *InFile;//打开绘图语言源程序时,指向该源程序的指针
  7 static char TokenBuffer[TOKEN_LEN];//设置一个字符缓冲区,用来保留记号的字符串,当需要记号的字符串时,char*lexeme指针会指向TokenBuffer
  8 
  9 //--------------------初始化词法分析器
 10 extern int InitScanner(const char *FileName)//输入要分析的源程序文件名
 11 {
 12     LineNo = 1;
 13     InFile = fopen(FileName, "r");
 14     if (InFile != NULL)
 15         return 1;                   //如果存在,打开文件,并初始化lineNO的值为1,返回true
 16     else
 17         return 0;//不存在返回0
 18 }
 19 
 20 //---------------------关闭词法分析器
 21 extern void CloseScanner(void)
 22 {
 23     if (InFile != NULL)
 24         fclose(InFile);
 25 }
 26 
 27 //--------------------从输入源程序中读入一个字符
 28 static char GetChar(void)
 29 {
 30     int Char = getc(InFile);
 31     return toupper(Char);//输出源程序的一个字符,没有输入
 32 }
 33 
 34 //--------------------把预读的字符退回到输入源程序中,分析的过程中需要预读1、2...个字符,预读的字符必须回退,以此保证下次读时不会丢掉字符
 35 static void BackChar(char Char)//输入:回退一个字符,  没有输出
 36 {
 37     if (Char != EOF)
 38         ungetc(Char, InFile);
 39 }
 40 
 41 //--------------------加入字符到TokenBuffer-----把已经识别的字符加到TokenBuffer 
 42 static void AddCharTokenString(char Char)//输入源程序的一个字符   没有输出
 43 {
 44     int TokenLength = strlen(TokenBuffer);//设定好长度
 45     if (TokenLength + 1 >= sizeof(TokenBuffer))
 46         return;//此时字符串的长度超过最大值,返回错误
 47     TokenBuffer[TokenLength] = Char;//添加一个字符
 48     TokenBuffer[TokenLength + 1] = '';
 49 }
 50 
 51 //--------------------请空记号缓冲区
 52 static void EmptyTokenString()
 53 {
 54     memset(TokenBuffer, 0, TOKEN_LEN);
 55 }
 56 
 57 //--------------------根据识别的字符串在符号表中查找相应的记号
 58 static Token JudgeKeyToken(const char *IDString)//输入:识别出的字符串;输出:记号
 59 {
 60     int loop;
 61     for (loop = 0;loop < sizeof(TokenTab) / sizeof(TokenTab[0]);loop++)
 62         if (strcmp(TokenTab[loop].lexeme, IDString) == 0)
 63             return TokenTab[loop];//查找成功,返回该记号
 64     Token errortoken;
 65     memset(&errortoken, 0, sizeof(Token));
 66     //void *memset(void *s, int ch, size_t n);
 67 //    函数解释:将s中前n个字节替换为ch并返回s;
 68 //    memset : 作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法。
 69     errortoken.type = ERRTOKEN;
 70     return errortoken;//查找失败,返回错误记号
 71 }
 72 
 73 //--------------------获取一个记号
 74 extern Token GetToken(void)//次函数由DFA转化而来。此函数输出一个记号。每调用该函数一次,仅仅获得一个记号。
 75 //因此,要获得源程序的所有记号,就要重复调用这个函数。下面声明的函数都被此函数调用过!
 76 //输出一个记号,没有输入
 77 {
 78     Token token;
 79     int Char;
 80     
 81     memset(&token, 0, sizeof(Token));
 82     EmptyTokenString();//清空缓冲区
 83     token.lexeme = TokenBuffer;//记号的字符指针指向字符缓冲区
 84     for (;;)
 85     {
 86         Char = GetChar();//从源程序中读出一个字符
 87         if(Char==EOF)
 88         {
 89             token.type = NONTOKEN;
 90             return token;
 91         }
 92         if (Char == '
')
 93             LineNo++;
 94         if (!isspace(Char))
 95             break;
 96     }//end of for
 97     AddCharTokenString(Char);
 98     //若不是空格、TAB、回车、文件结束符等,则先加入到记号的字符缓冲区中
 99     if (isalpha(Char))//判断是英文字母            //若char是A-Za-z,则一定是函数,关键字、PI、E等
100     {
101         for (;;)
102         {
103             Char = GetChar();
104             if (isalnum(Char))
105                 AddCharTokenString(Char);
106             else
107                 break;
108         }
109         BackChar(Char);
110         token = JudgeKeyToken(TokenBuffer);
111         token.lexeme = TokenBuffer;
112         return token;
113     }
114     else if (isdigit(Char))//判断是数字                    //若是一个数字,则一定是常量
115     {
116         for (;;)
117         {
118             Char = GetChar();
119             if (isdigit(Char))
120                 AddCharTokenString(Char);
121             else
122                 break;
123         }
124         if (Char == '.')
125         {
126             AddCharTokenString(Char);
127             for (;;)
128             {
129                 Char = GetChar();
130                 if (isdigit(Char))
131                     AddCharTokenString(Char);
132                 else
133                     break;
134             }
135         }
136         BackChar(Char);
137         token.type = CONST_ID;
138         token.value = atof(TokenBuffer);
139         return token;
140     }
141     else                                                    //不是字母和数字,则一定是运算符或者分隔符
142     {
143         switch (Char)
144         {
145         case ';':token.type = SEMICO;break;
146         case '(':token.type = L_BRACKET;break;
147         case ')':token.type = R_BRACKET;break;
148         case ',':token.type = COMMA;break;
149         case '+':token.type = PLUS;break;
150         case '-':
151             Char = GetChar();
152             if (Char == '-')
153             {
154                 while (Char != '
'&&HUGE != EOF)
155                     Char = GetChar();
156                 BackChar(Char);
157                 return GetToken();
158             }
159             else
160             {
161                 BackChar(Char);
162                 token.type = MINUS;
163                 break;
164             }
165         case '/':
166             Char = GetChar();
167             if (Char == '/')
168             {
169                 while (Char != '
'&&Char != EOF)
170                     Char = GetChar();
171                 BackChar(Char);
172                 return GetToken();
173             }
174             else
175             {
176                 BackChar(Char);
177                 token.type = DIV;
178                 break;
179             }
180         case '*':
181             Char = GetChar();
182             if (Char == '*')
183             {
184                 token.type = POWER;
185                 break;
186             }
187             else
188             {
189                 BackChar(Char);
190                 token.type = MUL;
191                 break;
192             }
193         default:token.type = ERRTOKEN;break;
194         }//end of switch
195     }//end of else
196     return token;
197 }//end of GetToken
198 #endif
 1 #include"scanner.h"
 2 using namespace std;
 3 void main()
 4 {
 5     Token token;
 6     char file[] = "test0.txt";
 7     if (!InitScanner(file))                                    //初始化词法分析器
 8     {
 9         printf("Open Sorce File Error !
");
10         return;
11     }
12     printf("记号类别    字符串      常数值     函数指针
");
13     printf("--------------------------------------------
");
14     while (true)
15     {
16         token = GetToken();//输出一个记号
17         if (token.type != NONTOKEN)//记号的类别不是错误,就打印出他的内容
18             printf("%4d,%12s,%12f,%12x
", token.type, token.lexeme, token.value, token.FuncPtr);
19         else
20             break;
21     }
22     printf("-------------------------------------------
");
23     getchar();
24     //.当程序调用getchar时.程序就等着用户按键.........没有这个,黑框会闪一下
25     CloseScanner();
26     system("pause");
27 }

有了对词法分析器的分析和详细大量的注解,应该不难看懂。下面说说这个工程的细节。

step 4

1、这是我遇到最多的一个问题,也就是这个问题反复请教小哥哥的。

我们来看看报错

错误1error C4996: 'fopen': This function or variable may be unsafe. Consider using fopen_s instead. To disable depreca

解决方案:1.项目 ->属性 -> c/c++ -> 预处理器 -> 点击预处理器定义,编辑,加入_CRT_SECURE_NO_WARNINGS

2.在scanner.h中定义_CRT_SECURE_NO_WARNINGS

2、测试程序:通过更改scannermain.cpp中的file字符数组来改变要读取的文件

eg:test0

FOR t FROM 0 TO 2*PI STEP PI/50 DRAW(COS(t),sin(t));

运行结果如下:

----the end----

再次感谢萌萌小哥哥对我的帮助 o(* ̄▽ ̄*)o

原文地址:https://www.cnblogs.com/olivegyr/p/6189753.html