辣雞的我終於在一個已經保研的小哥哥(萌似泰迪)的幫助下完成了解釋器!!(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