編譯原理解釋器(一)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] = '\0';
 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 == '\n')
 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 != '\n'&&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 != '\n'&&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 !\n");
10         return;
11     }
12     printf("記號類別    字符串      常數值     函數指針\n");
13     printf("--------------------------------------------\n");
14     while (true)
15     {
16         token = GetToken();//輸出一個記號
17         if (token.type != NONTOKEN)//記號的類別不是錯誤,就打印出他的內容
18             printf("%4d,%12s,%12f,%12x\n", token.type, token.lexeme, token.value, token.FuncPtr);
19         else
20             break;
21     }
22     printf("-------------------------------------------\n");
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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM