編譯原理——詞法分析器實現


詞法分析器實現

一、寫在前面

  編譯原理是軟件工程的一項基礎的課程,是研究軟件是什么,為什么可以運行,以及怎么運行的學科,編譯系統的改進將會直接對其上層的應用程序的執行效率,執行原理產生深刻的影響。編譯原理的目的是將源語言翻譯成目標語言。與翻譯的區別就是,編譯將高級語言編譯成低級語言。至於達到什么樣的低級語言,在不同的系統中是不同的,對於不同的機器都要用相應的指令系統,編譯的目的就是將編譯出來的語言用目標機的指令系統執行,一般而言是翻譯到匯編語言的層次,但也有特例,比如JVM,Java虛擬機是將高級語言編譯到中間語言環節,對於任何的高級語言,都翻譯成相同的自己可以識別的中間語言,這樣就可以在不同的機型上運行了,這種獨特的創意造就了與平台無關的語言識別器——虛擬機的出現,從本質上來說也是用到了編譯原理。編譯原理的內容非常豐富,技術非常成熟,有着幾十年的研究歷史,筆者在大學學習《編譯原理》的時候,僥幸在最后的期末考試中獲得了滿分的優異成績,這樣一門內容豐富、邏輯嚴謹、極度抽象和形式化的課程,筆者是怎么學的呢,無外乎兩個字——興趣。只要有興趣,任何困難都會顯得微不足道!!!轉瞬之間,大學已接近尾聲,在即將踏出校門的一刻,突然有種什么事情沒有完成的感覺,仔細想想,自己在大學學了很多的知識,自己究竟掌握的怎么樣了,是不是很好的收集和整理了?想到這里,不覺出了冷汗,因此,在以后的空閑時間就把自己學到的知識,覺得有意思的東西拿出來給大家欣賞,一是為了自己以后記憶,二是為了交流和共享。我一直相信這個時代最偉大的一種變革就是交流和共享,因為有溝通,有彼此的相互了解,相互學習才能打破人類歷史幾千年來的閉門造車、敝帚自珍,人人都獻出一點有用的、精華的信息,隨着時代的發展,幾十年,上百年之后,文明將會變得更加的璀璨和瑰麗,這個世界將變得更加美好!在這個系列中,我將拿出兩個貫穿編譯原理的例子來讓大家能夠比較輕松的理解編譯上的問題,一個是詞法分析器,另一個是在詞法分析的基礎上,進行算符優先分析文法的語法分析,並且生成中間代碼,執行代碼的例子,這兩個例子都是本人經過了十來天的編程調試,用C/C++實現的,便於理解,有着很好的學習指導意義!

二、詞法分析器的實現

  2.1、設計題目:手工設計c語言的詞法分析器           (可以是c語言的子集)

  2.2、設計內容: 處理c語言源程序,過濾掉無用符號,判斷源程序中單詞的合法性,並分解出正確的單詞,以二元組形式存放在文件中。

  2.3、設計目的: 了解高級語言單詞的分類,了解狀態圖以及如何表示並識別單詞規則,掌握狀態圖到識別程序的編程。

  2.4、分析與設計

   要想手工設計詞法分析器,實現C語言子集的識別,就要明白什么是詞法分析器,它的功能是什么。詞法分析是編譯程序進行編譯時第一個要進行的任務,主要是對源程序進行編譯預處理(去除注釋、無用的回車換行找到包含的文件等)之后,對整個源程序進行分解,分解成一個個單詞,這些單詞有且只有五類,分別是標識符、保留字、常數、運算符、界符。以便為下面的語法分析和語義分析做准備。可以說詞法分析面向的對象是單個的字符,目的是把它們組成有效的單詞(字符串);而語法的分析則是利用詞法分析的結果作為輸入來分析是否符合語法規則並且進行語法制導下的語義分析,最后產生四元組(中間代碼),進行優化(可有可無)之后最終生成目標代碼。可見詞法分析是所有后續工作的基礎,如果這一步出錯,比如明明是‘<=’卻被拆分成‘<’和‘=’就會對下文造成不可挽回的影響。因此,在進行詞法分析的時候一定要定義好這五種符號的集合。下面是我構造的一個C語言子集。

第一類:標識符   letter(letter | digit)*  無窮集
第二類:常數    (digit)+  無窮集
第三類:保留字(32)
auto       break    case     char        const      continue  
default    do       double   else        enum       extern    
float      for      goto     if          int        long      
register   return   short    signed      sizeof     static  
struct     switch   typedef  union       unsigned   void  
volatile    while
第四類:界符  ‘/*’、‘//’、 () { } [ ] " "  ' 等
第五類:運算符 <、<=、>、>=、=、+、-、*、/、^、等
對所有可數符號進行編碼:
<$,0>
<auto,1>
...
<while,32>
<+,33>
<-,34>
<*,35>
</,36>
<<,37>
<<=,38>
<>,39>
<>=,40>
<=,41>
<==,42>
<!=,43>
<;,44>
<(,45>
<),46>
<^,47>
<,,48>
<",49>
<',50>
<#,51>
<&,52>
<&&,53>
<|,54>
<||,55>
<%,56>
<~,57>
<<<,58>左移
<>>,59>右移
<[,60>
<],61>
<{,62>
<},63>
<\,64>
<.,65>
<?,66>
<:,67>
<!,68>
"[","]","{","}"
<常數99  ,數值>
<標識符100 ,標識符指針>
   上述二元組中左邊是單詞的符號,右邊為其種別碼,其中常數和標識符有點特別,因為是無窮集合,因此常數用自身來表示,種別碼為99,標識符用標識符符號表的指針表示(當然也可用自身顯示,比較容易觀察),種別碼100。根據上述約定,一旦見到了種別碼syn=63,就唯一確定了‘}’這個單詞。
下面是一些變量的約定:
//全局變量,保留字表
static char reserveWord[32][20] = {
    "auto", "break", "case", "char", "const", "continue",
    "default", "do", "double", "else", "enum", "extern",
    "float", "for", "goto", "if", "int", "long",
    "register", "return", "short", "signed", "sizeof", "static",
    "struct", "switch", "typedef", "union", "unsigned", "void",
    "volatile", "while"
};
//界符運算符表,根據需要可以自行增加
static char operatorOrDelimiter[36][10]={
   "+","-","*","/","<","<=",">",">=","=","==",
   "!=",";","(",")","^",",","\"","\'","#","&",
   "&&","|","||","%","~","<<",">>","[","]","{",
   "}","\\",".","\?",":","!"
};
static  char IDentifierTbl[1000][50]={""};//標識符表
char resourceProject[10000];//輸入的源程序存放處,最大可以存放10000個字符。
char token[20]={0};//每次掃描的時候存儲已經掃描的結果。
int syn=-1;//syn即為種別碼,約定‘$’的種別碼為0,為整個源程序的結束符號一旦掃描到這個字符代表掃描結束
int pProject = 0;//源程序指針,始終指向當前源程序待掃描位置。

幾個重要函數:
//查找保留字,若成功查找,則返回種別碼
//否則返回-1,代表查找不成功,即為標識符
int searchReserve(char reserveWord[ ][20], char s[])
/*********************判斷是否為字母********************/
bool IsLetter(char letter)
/*****************判斷是否為數字************************/
bool IsDigit(char digit)
/********************編譯預處理,取出無用的字符和注釋**********************/
void filterResource(char r[],int pProject)
/****************************分析子程序,算法核心***********************/
void Scanner(int &syn,char resourceProject[],char token[],int &pProject)

下面說一下整個程序的流程:
   1.詞法分析程序打開源文件,讀取文件內容,直至遇上’$’文件結束符,然后讀取結束。
   2.對讀取的文件進行預處理,從頭到尾進行掃描,去除//和/*  */的內容,以及一些無用的、影響程序執行的符號如換行符、回車符、制表符等。但是千萬注意不要在這個時候去除空格,因為空格在詞法分析中有用,比如說int i=3;這個語句,如果去除空格就變成了“inti=3”,這樣就失去了程序的本意,因此不能在這個時候去除空格。
  3.選下面就要對源文件從頭到尾進行掃描了,從頭開始掃描,這個時候掃描程序首先要詢問當前的字符是不是空格,若是空格,則繼續掃描下一個字符,直至不是空格,然后詢問這個字符是不是字母,若是則進行標識符和保留字的識別;若這個字符為數字,則進行數字的判斷。否則,依次對這個字符可能的情況進行判斷,若是將所有可能都走了一遍還是沒有知道它是誰,則認定為錯誤符號,輸出該錯誤符號,然后結束。每次成功識別了一個單詞后,單詞都會存在token[ ]中。然后確定這個單詞的種別碼,最后進行下一個單詞的識別。這就是掃描程序進行的工作,可以說這個程序徹底實現了確定有限自動機的某些功能,比如說識別標識符,識別數字等。為了簡單起見,這里的數字只是整數。
  4.主控程序主要負責對每次識別的種別碼syn進行判斷,對於不同的單詞種別做出不同的反應,如對於標識符則將其插入標識符表中。對於保留字則輸出該保留字的種別碼和助記符,等等吧。直至遇到syn=0;程序結束。

  2.5、流程圖
    下面是程序的流程圖:

 2.6、運行與測試
   比如說,就拿這個源程序的一部分進行測試:

運行程序后結果為:

同樣單詞也寫入了文件如下:

。。。

綜上分析,達到了預期的結果。

  2.7、實驗體會
    
每做一次比較大的實驗,都應該寫一下實驗體會,來加深自己對知識的認識。其實這次的實驗,算法部分並不難,只要知道了DFA,這個模塊很好寫,比較麻煩的就是五種類型的字符個數越多程序就越長。但為了能識別大部分程序,我還是用了比較大的子集,結果花了一下午的功夫才寫完,雖然很累吧,但看着這個詞法分析器的處理能力,覺得還是值得的。同時也加深了對字符的認識。程序的可讀性還算不錯。程序沒有實現的是對所有復合運算的分離,但原理是相同的,比如“+=“,只需在”+“的邏輯之后向前掃描就行了,因此就沒有再加上了。感受最深的是學習編譯原理必須要做實驗,寫程序,這樣才會提高自己的動手能力,加深自己對難點的理解,對於以后的求first{},follow{},fisrtVT{},lastVT{}更是應該如此。

  2.8、源程序

復制代碼
  1 // Lexical_Analysis.cpp : 定義控制台應用程序的入口點。
  2 //
  3 #include "stdio.h"
  4 #include "stdlib.h"
  5 #include "string.h"
  6 #include "iostream"
  7 using namespace std;
  8 //詞法分析程序
  9 //首先定義種別碼
 10 /*
 11 第一類:標識符   letter(letter | digit)*  無窮集
 12 第二類:常數    (digit)+  無窮集
 13 第三類:保留字(32)
 14 auto       break    case     char        const      continue
 15 default    do       double   else        enum       extern
 16 float      for      goto     if          int        long
 17 register   return   short    signed      sizeof     static
 18 struct     switch   typedef  union       unsigned   void
 19 volatile    while
 20 
 21 第四類:界符  ‘/*’、‘//’、 () { } [ ] " "  '
 22 第五類:運算符 <、<=、>、>=、=、+、-、*、/、^、
 23 
 24 對所有可數符號進行編碼:
 25 <$,0>
 26 <auto,1>
 27 ...
 28 <while,32>
 29 <+,33>
 30 <-,34>
 31 <*,35>
 32 </,36>
 33 <<,37>
 34 <<=,38>
 35 <>,39>
 36 <>=,40>
 37 <=,41>
 38 <==,42>
 39 <!=,43>
 40 <;,44>
 41 <(,45>
 42 <),46>
 43 <^,47>
 44 <,,48>
 45 <",49>
 46 <',50>
 47 <#,51>
 48 <&,52>
 49 <&&,53>
 50 <|,54>
 51 <||,55>
 52 <%,56>
 53 <~,57>
 54 <<<,58>左移
 55 <>>,59>右移
 56 <[,60>
 57 <],61>
 58 <{,62>
 59 <},63>
 60 <\,64>
 61 <.,65>
 62 <?,66>
 63 <:,67>
 64 <!,68>
 65 "[","]","{","}"
 66 <常數99  ,數值>
 67 <標識符100 ,標識符指針>
 68 
 69 
 70 */
 71 
 72 /****************************************************************************************/
 73 //全局變量,保留字表
 74 static char reserveWord[32][20] = {
 75     "auto", "break", "case", "char", "const", "continue",
 76     "default", "do", "double", "else", "enum", "extern",
 77     "float", "for", "goto", "if", "int", "long",
 78     "register", "return", "short", "signed", "sizeof", "static",
 79     "struct", "switch", "typedef", "union", "unsigned", "void",
 80     "volatile", "while"
 81 };
 82 //界符運算符表,根據需要可以自行增加
 83 static char operatorOrDelimiter[36][10] = {
 84     "+", "-", "*", "/", "<", "<=", ">", ">=", "=", "==",
 85     "!=", ";", "(", ")", "^", ",", "\"", "\'", "#", "&",
 86     "&&", "|", "||", "%", "~", "<<", ">>", "[", "]", "{",
 87     "}", "\\", ".", "\?", ":", "!"
 88 };
 89 
 90 static  char IDentifierTbl[1000][50] = { "" };//標識符表
 91 /****************************************************************************************/
 92 
 93 /********查找保留字*****************/
 94 int searchReserve(char reserveWord[][20], char s[])
 95 {
 96     for (int i = 0; i < 32; i++)
 97     {
 98         if (strcmp(reserveWord[i], s) == 0)
 99         {//若成功查找,則返回種別碼
100             return i + 1;//返回種別碼
101         }
102     }
103     return -1;//否則返回-1,代表查找不成功,即為標識符
104 }
105 /********查找保留字*****************/
106 
107 /*********************判斷是否為字母********************/
108 bool IsLetter(char letter)
109 {//注意C語言允許下划線也為標識符的一部分可以放在首部或其他地方
110     if (letter >= 'a'&&letter <= 'z' || letter >= 'A'&&letter <= 'Z'|| letter=='_')
111     {
112         return true;
113     }
114     else
115     {
116         return false;
117     }
118 }
119 /*********************判斷是否為字母********************/
120 
121 
122 /*****************判斷是否為數字************************/
123 bool IsDigit(char digit)
124 {
125     if (digit >= '0'&&digit <= '9')
126     {
127         return true;
128     }
129     else
130     {
131         return false;
132     }
133 }
134 /*****************判斷是否為數字************************/
135 
136 
137 /********************編譯預處理,取出無用的字符和注釋**********************/
138 void filterResource(char r[], int pProject)
139 {
140     char tempString[10000];
141     int count = 0;
142     for (int i = 0; i <= pProject; i++)
143     {
144         if (r[i] == '/'&&r[i + 1] == '/')
145         {//若為單行注釋“//”,則去除注釋后面的東西,直至遇到回車換行
146             while (r[i] != '\n')
147             {
148                 i++;//向后掃描
149             }
150         }
151         if (r[i] == '/'&&r[i + 1] == '*')
152         {//若為多行注釋“/* 。。。*/”則去除該內容
153             i += 2;
154             while (r[i] != '*' || r[i + 1] != '/')
155             {
156                 i++;//繼續掃描
157                 if (r[i] == '$')
158                 {
159                     printf("注釋出錯,沒有找到 */,程序結束!!!\n");
160                     exit(0);
161                 }
162             }
163             i += 2;//跨過“*/”
164         }
165         if (r[i] != '\n'&&r[i] != '\t'&&r[i] != '\v'&&r[i] != '\r')
166         {//若出現無用字符,則過濾;否則加載
167             tempString[count++] = r[i];
168         }
169     }
170     tempString[count] = '\0';
171     strcpy(r, tempString);//產生凈化之后的源程序
172 }
173 /********************編譯預處理,取出無用的字符和注釋**********************/
174 
175 
176 /****************************分析子程序,算法核心***********************/
177 void Scanner(int &syn, char resourceProject[], char token[], int &pProject)
178 {//根據DFA的狀態轉換圖設計
179     int i, count = 0;//count用來做token[]的指示器,收集有用字符
180     char ch;//作為判斷使用
181     ch = resourceProject[pProject];
182     while (ch == ' ')
183     {//過濾空格,防止程序因識別不了空格而結束
184         pProject++;
185         ch = resourceProject[pProject];
186     }
187     for (i = 0; i<20; i++)
188     {//每次收集前先清零
189         token[i] = '\0';
190     }
191     if (IsLetter(resourceProject[pProject]))
192     {//開頭為字母
193         token[count++] = resourceProject[pProject];//收集
194         pProject++;//下移
195         while (IsLetter(resourceProject[pProject]) || IsDigit(resourceProject[pProject]))
196         {//后跟字母或數字
197             token[count++] = resourceProject[pProject];//收集
198             pProject++;//下移
199         }//多讀了一個字符既是下次將要開始的指針位置
200         token[count] = '\0';
201         syn = searchReserve(reserveWord, token);//查表找到種別碼
202         if (syn == -1)
203         {//若不是保留字則是標識符
204             syn = 100;//標識符種別碼
205         }
206         return;
207     }
208     else if (IsDigit(resourceProject[pProject]))
209     {//首字符為數字
210         while (IsDigit(resourceProject[pProject]))
211         {//后跟數字
212             token[count++] = resourceProject[pProject];//收集
213             pProject++;
214         }//多讀了一個字符既是下次將要開始的指針位置
215         token[count] = '\0';
216         syn = 99;//常數種別碼
217     }
218     else if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == ';' || ch == '(' || ch == ')' || ch == '^'
219         || ch == ',' || ch == '\"' || ch == '\'' || ch == '~' || ch == '#' || ch == '%' || ch == '['
220         || ch == ']' || ch == '{' || ch == '}' || ch == '\\' || ch == '.' || ch == '\?' || ch == ':')
221     {//若為運算符或者界符,查表得到結果
222         token[0] = resourceProject[pProject];
223         token[1] = '\0';//形成單字符串
224         for (i = 0; i<36; i++)
225         {//查運算符界符表
226             if (strcmp(token, operatorOrDelimiter[i]) == 0)
227             {
228                 syn = 33 + i;//獲得種別碼,使用了一點技巧,使之呈線性映射
229                 break;//查到即推出
230             }
231         }
232         pProject++;//指針下移,為下一掃描做准備
233         return;
234     }
235     else  if (resourceProject[pProject] == '<')
236     {//<,<=,<<
237         pProject++;//后移,超前搜索
238         if (resourceProject[pProject] == '=')
239         {
240             syn = 38;
241         }
242         else if (resourceProject[pProject] == '<')
243         {//左移
244             pProject--;
245             syn = 58;
246         }
247         else
248         {
249             pProject--;
250             syn = 37;
251         }
252         pProject++;//指針下移
253         return;
254     }
255     else  if (resourceProject[pProject] == '>')
256     {//>,>=,>>
257         pProject++;
258         if (resourceProject[pProject] == '=')
259         {
260             syn = 40;
261         }
262         else if (resourceProject[pProject] == '>')
263         {
264             syn = 59;
265         }
266         else
267         {
268             pProject--;
269             syn = 39;
270         }
271         pProject++;
272         return;
273     }
274     else  if (resourceProject[pProject] == '=')
275     {//=.==
276         pProject++;
277         if (resourceProject[pProject] == '=')
278         {
279             syn = 42;
280         }
281         else
282         {
283             pProject--;
284             syn = 41;
285         }
286         pProject++;
287         return;
288     }
289     else  if (resourceProject[pProject] == '!')
290     {//!,!=
291         pProject++;
292         if (resourceProject[pProject] == '=')
293         {
294             syn = 43;
295         }
296         else
297         {
298             syn = 68;
299             pProject--;
300         }
301         pProject++;
302         return;
303     }
304     else  if (resourceProject[pProject] == '&')
305     {//&,&&
306         pProject++;
307         if (resourceProject[pProject] == '&')
308         {
309             syn = 53;
310         }
311         else
312         {
313             pProject--;
314             syn = 52;
315         }
316         pProject++;
317         return;
318     }
319     else  if (resourceProject[pProject] == '|')
320     {//|,||
321         pProject++;
322         if (resourceProject[pProject] == '|')
323         {
324             syn = 55;
325         }
326         else
327         {
328             pProject--;
329             syn = 54;
330         }
331         pProject++;
332         return;
333     }
334     else  if (resourceProject[pProject] == '$')
335     {//結束符
336         syn = 0;//種別碼為0
337     }
338     else
339     {//不能被以上詞法分析識別,則出錯。
340         printf("error:there is no exist %c \n", ch);
341         exit(0);
342     }
343 }
344 
345 
346 int main()
347 {
348     //打開一個文件,讀取其中的源程序
349     char resourceProject[10000];
350     char token[20] = { 0 };
351     int syn = -1, i;//初始化
352     int pProject = 0;//源程序指針
353     FILE *fp, *fp1;
354     if ((fp = fopen("D:\\zyr_rc.txt", "r")) == NULL)
355     {//打開源程序
356         cout << "can't open this file";
357         exit(0);
358     }
359     resourceProject[pProject] = fgetc(fp);
360     while (resourceProject[pProject] != '$')
361     {//將源程序讀入resourceProject[]數組
362         pProject++;
363         resourceProject[pProject] = fgetc(fp);
364     }
365     resourceProject[++pProject] = '\0';
366     fclose(fp);
367     cout << endl << "源程序為:" << endl;
368     cout << resourceProject << endl;
369     //對源程序進行過濾
370     filterResource(resourceProject, pProject);
371     cout << endl << "過濾之后的程序:" << endl;
372     cout << resourceProject << endl;
373     pProject = 0;//從頭開始讀
374 
375     if ((fp1 = fopen("D:\\zyr_compile.txt", "w+")) == NULL)
376     {//打開源程序
377         cout << "can't open this file";
378         exit(0);
379     }
380     while (syn != 0)
381     {
382         //啟動掃描
383         Scanner(syn, resourceProject, token, pProject);
384         if (syn == 100)
385         {//標識符
386             for (i = 0; i<1000; i++)
387             {//插入標識符表中
388                 if (strcmp(IDentifierTbl[i], token) == 0)
389                 {//已在表中
390                     break;
391                 }
392                 if (strcmp(IDentifierTbl[i], "") == 0)
393                 {//查找空間
394                     strcpy(IDentifierTbl[i], token);
395                     break;
396                 }
397             }
398             printf("(標識符  ,%s)\n", token);
399             fprintf(fp1, "(標識符   ,%s)\n", token);
400         }
401         else if (syn >= 1 && syn <= 32)
402         {//保留字
403             printf("(%s   ,  --)\n", reserveWord[syn - 1]);
404             fprintf(fp1, "(%s   ,  --)\n", reserveWord[syn - 1]);
405         }
406         else if (syn == 99)
407         {//const 常數
408             printf("(常數   ,   %s)\n", token);
409             fprintf(fp1, "(常數   ,   %s)\n", token);
410         }
411         else if (syn >= 33 && syn <= 68)
412         {
413             printf("(%s   ,   --)\n", operatorOrDelimiter[syn - 33]);
414             fprintf(fp1, "(%s   ,   --)\n", operatorOrDelimiter[syn - 33]);
415         }
416     }
417     for (i = 0; i<100; i++)
418     {//插入標識符表中
419         printf("第%d個標識符:  %s\n", i + 1, IDentifierTbl[i]);
420         fprintf(fp1, "第%d個標識符:  %s\n", i + 1, IDentifierTbl[i]);
421     }
422     fclose(fp1);
423     return 0;
424 }
復制代碼


免責聲明!

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



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