目錄
1 編譯器
1.1 定義
編譯器就是將高級程序語言翻譯成機器語言的工具。可以理解爲機器語言是給計算機看的,比較複雜。所以出現了C語言、Java等高級語言,這些語言是更適合人理解的東西,更符合人的邏輯。雖然人能更容易的看懂,但是計算機不知道是什麼東西,所以需要一個東西進行翻譯,這就是編譯器。
1.2 編譯過程
- 詞法分析:識別單詞
- 語法分析:組詞成句
- 語義分析:分析句子是否正確
- 生成中間代碼
- 代碼優化:提高代碼效率
- 生成目標代碼:最終能夠執行的東西
1.3 需要實現的功能
- 實現對程序中錯誤的反應,編譯的每個階段都要檢查相應的錯誤。
- 對沒有問題的程序,生成目標代碼
2 TEST測試語言
TEST語言一種測試語言,類似C語言,但簡單很多。拿來進行編譯原理的學習理解很有幫助
2.1 TEST詞法規則
詞法規則表明了TEST語言中不同類型的單詞都是由哪些符號構成,例如< NUM > 表示一個數字,它由< digit>或者< NUM> < digit> 構成,即一個數字或者數字串
< ID>∷=< letter>|< ID>< letter>|< ID>< digit>
< NUM>∷=< digit>|< NUM> < digit>
< letter>∷= a|b|…|z|A|B|…|Z
< digit>∷=1|2|…|9|0
< singleword>∷= + | - | * | / | = |(|)|{ | }|:|,|;| < | >| !
< doubleword>∷= >= |<= | != | ==|&&| ||
< commend_first>∷= /*
< commend_last>∷= */
2.2 TEST語法規則
語法規則表明了每一個句子長什麼樣子,組成一個句子的單詞都是以怎樣的順序排列的,例如<fun_declaration>表示一個函數,它由 function ID ’(‘ ‘ )’ < function_body> 構成,即函數名 + ‘(’ + ‘)’ + 函數體
(1). < program> ::={fun_declaration }<main_declaration>
(2). <fun_declaration>::= function ID’(‘ ‘ )’< function_body>
(3). <main_declaration>::=main’(‘ ‘ )’ < function_body>
(4). <function_body>::= ‘{’<declaration_list><statement_list> ‘}’
(5). <declaration_list>::=<declaration_list><declaration_stat> |ε <declaration_list>::={<declaration_stat>}
(6). <declaration_stat>::=int ID;
(7). <statement_list>::=<statement_list>| ε <statement_list>::={}
(8). < statement>::=<if_stat>|<while_stat>|<for_stat>|<read_stat> |<write_stat>|<compound_stat> |<expression_stat> | < call _stat>
(9). <if_stat>::= if ‘(‘’)’ [else < statement >]
(10). <while_stat>::= while ‘(‘’)’ < statement >
(11). <for_stat>::= for’(‘;;’)’
(12). <write_stat>::=write ;
(13). <read_stat>::=read ID;
(14). <compound_stat>::=’{‘<statement_list>’}’
(15). <expression_stat>::=< expression >;|;
(16). < call _stat>::= call ID‘(’ ‘)’
(17). < expression >::= ID=<bool_expr>|<bool_expr>
(18). < bool_expr>::=< additive_expr> | < additive_expr >(>|<|>=|<=|== | !=) < additive_expr > < bool_expr>::=< additive_expr >{(>|<|>=|<=|==|!=)< additive_expr>}
(19). < additive_expr>::={(+|-)< term >}
(20). < term >::={(*| /)< factor >}
(21). < factor >::=‘(’ < additive_expr >‘)’|ID|NUM
有了這些規則之後,只要學過編譯原理,就能看懂它們的含義,知道哪些符號是合法的、一個句子要怎麼構成、會出現哪些錯誤等等。然後根據這些規則就可以進行TEST編譯器的設計了
3 詞法分析
3.1 功能
- 根據詞法規則分析和識別單詞,將保留字、標識符、常數、運算符等加入到二元組中
- 跳過各種空字,例如空格、回車、製表符
- 刪去註釋
- 報告發現的詞法錯誤
- 最終將沒有詞法錯誤的二元組傳遞給語法分析
3.2 特點
- 輸入的是TEST語言寫的源代碼
- 輸出的是二元組形式的單詞流文件。二元組由類型和值構成,其中由於無法限制用戶會定義什麼樣的標識符、使用什麼樣的常數,二元組的類型則不可能全部包含在內,所以標識符要歸爲一類(ID),常數歸爲一類(NUM)。例如:ID a , NUM 123
3.3 錯誤類型
- 由詞法規則 < ID>∷=< letter>|< ID>< letter>|< ID>< digit> 可知,標識符可以由字母或字母+數字構成,但如果是以數字或其他非字母字符開頭就不符合這條規則,屬於詞法錯誤
- 詞法規則中使用到的符號有限,當出現沒有包含在規則內的字符時要當作非法字符,屬於詞法錯誤
- 註釋是用 /* 和 * / 包起來的,如果只有 /* 而沒有 */ 就會出現開始註釋符之後一直到程序末尾都作爲了註釋,可能會把原本不是註釋的內容當作註釋,所以缺少 */ 也應該屬於詞法錯誤
3.4 設計思路
-
詞法分析的過程是從源代碼文件中每次讀取一個字符,直到源代碼讀取完爲止。在這個過程中根據詞法規則將這些字符合併成一個個單詞(相當於在英語中把一個個字母拼湊成一個單詞),如果這個過程中有詞法錯誤就輸出錯誤,如果沒有問題就將根據構成的這個單詞類型和值構成二元組。整體的邏輯可以根據這張程序流程圖來處理
-
我認爲詞法分析的關鍵就是判斷何時構成了一個單詞。最簡單的就是通過空字符判斷,由於空字符是沒有意義的,所以兩個字符之間用空格、回車等隔開,則這兩個字符一定是屬於兩個不同的單詞的;其次,如果前後兩個字符屬於不同類型單詞,則這兩個字符可能是屬於兩個不同的單詞,例如 abc>1,abc 和 > 屬於兩個不同的類型,則就是兩個不同的單詞。這個情況不是絕對的,例如標識符可以由字母和數字組成,則標識符 a1 就只是一個單詞,不能分開看
-
標識符含有字母和數字,但數字和字母會分開:
解決方法是在標識符的判斷語句中多加一個可能:如果上一個字符是字母而當前字符是數字那麼就有可能是同一個標識符的內容。但僅僅這樣判斷還會有一個問題,如果數字在字母中間又會出錯。例如當內容是 int a2a=2; 時,可能還是會分開成兩個標識符。出現該問題的原因是當判斷第二個a的時候會先檢查前一個是不是字母,如果不是就證明之前的內容不是同一類型可以輸出了。在這個地方就會檢測出上一個字符2不是字母就會誤判爲第二個a和之前的a2不是同一類型的,就會輸出a2,再輸出a。解決方法就是增加一個檢測條件:如果當前暫時存儲的標識符數組長度爲0就輸出之前的內容。如果長度不爲0說明之前是有標識符還未輸出,當前的字符可能是屬於同一個標識符裏的。 -
末尾的 ’}’ 會輸出兩次:
原因是到了最後一次分析時已經沒有任何字符了,只能用上一次的 ’}’ ,就導致末尾多輸出一次。解決辦法是每次處理完一個字符後就把當前字符設置爲空格,及時多進行一次讀取也是讀到空字會直接忽視掉。 -
註釋處理:
我最開始考慮的是用兩個flag分別標記開始註釋和註釋結束,但後面發現並不需要記錄結束的flag,因爲在註釋中的內容就沒有必要進行詞法分析,但是要不斷更新上一個字符和當前字符的值。當上一個字符爲*並且當前字符爲/時,那麼就說明到註釋的末尾了,之後的內容就可以恢復正常的詞法分析了。 -
統一輸出錯誤集合:
爲了得到友好的交互界面,可以把遇到每一個錯誤都保存到錯誤集合中,等詞法分析完後最後再一次性輸出。在把錯誤存到集合中時,由於存在一些錯誤信息是由變量保存的,我最開始使用了strcat()函數不斷拼接錯誤信息,但這樣太麻煩了,於是改用sprintf()函數就可以一次性把要保存的錯誤信息添加到集合中
例如,sprintf(errors[errorSum], “第%d行: 非法標識符%s”, line, character); 第一個參數是字符串要存放的地址,後面的兩個參數就和printf的一樣。可以理解爲printf是將輸出內容直接輸出在控制檯,而sprintf是將輸出內容保存到一個地方中,但兩個的操作方法都一樣。這樣一來即使存在變量也可以一次性將字符串組合起來
4 完整代碼
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std ;
const int keywordNum = 15 ; //保留字個數
const int singlewordNum = 9 ; //單分界符個數
const int pureSingleNum = 6 ; //純單分符個數
const int doublewordNum = 6 ; //雙分界符個數
char identifier[105] ; //記錄當前的標識符
int identifierLength = 0 ; //標識符長度
int keywordPlace ; //保留字的位置
char numbers[105] ; //記錄當前數字大小
int numbersLength = 0 ; //數字位數
char character[10] ; //記錄目前分界符
int characterNum = 0 ; //分界符數
char note[2] ; //記錄可能是註釋的字符
int noteNum = 0 ; //註釋數
bool startFlag = false ; //標記是否進入註釋
char prior ; //上一個讀取的字符
int errorSum = 0 ; //報錯數量
int line = 1 ; //源程序行數
char inputAddress[20] ; //輸入文件地址
char outputAddress[20] ; //輸出文件地址
FILE *inputFile ; //輸入test文件
FILE *outputFile ; //輸出結果文件
char keyword[keywordNum][20] = {"break", "call", "case", "default", "do",
"else", "for", "function", "if", "int", "read", "switch", "then", "while", "write" } ; //保留字,按字典序排好
char pureSingleword[pureSingleNum] = {'(', ')', ',', ';', '{', '}'} ; //純單分符
char singleword[singlewordNum] = {'!', '&', '*', '+', '-', '<', '=', '>', '|'} ;//單分符或可能存在雙分符
char doubleword[doublewordNum][5] = {"!=", "&&", "<=", "==", ">=", "||"} ; //雙分符
char errors[105][50] ;
//折半查找保留字,要使用必須先把保留字表按從小到大的順序排好
bool binarySearch(char temp[], int left, int right) {
if(left > right) //遍歷所有元素都找不到
return false ;
int mid = (left+right) / 2 ;
if(!strcmp(temp, keyword[mid])) { //找到和暫時存儲在標識符數組相同的就是保留字
keywordPlace = mid ;
return true ;
}
else if(strcmp(temp, keyword[mid]) > 0)
return binarySearch(temp, mid+1, right) ;
else
return binarySearch(temp, left, mid-1) ;
}
//判斷字符是否爲空字
bool isNull(char temp) {
if(temp=='\n') {
line++ ; //每有一個回車就加一行
return true ;
}
if(temp==' ' || temp=='\t' || temp=='\n')//空字包括空格、回車、tab
return true ;
else
return false ;
}
//判斷字符是否是字母
bool isLetter(char temp) {
if(temp<='z' && temp>='a') //小寫字母
return true ;
if(temp<='Z' && temp>='A') //大寫字母
return true ;
return false ;
}
//判斷字符是否是數字
bool isNumber(char temp) {
if(temp<='9' && temp>='0')
return true ;
return false ;
}
//判斷是否是純單分符
bool isPureSingleword(char temp) {
for(int i=0; i<pureSingleNum; i++) { //查已經寫好的純單分符表有相同的就是
if(temp == pureSingleword[i])
return true ;
}
return false ;
}
//判斷是否是單分符
bool isSingleword(char temp) {
for(int i=0; i<singlewordNum; i++) { //查已經寫好的單分符表有相同的就是
if(temp == singleword[i])
return true ;
}
return false ;
}
//判斷是否是單雙分符
bool isDoubleword() {
for(int i=0; i<doublewordNum; i++) { //查已經寫好的雙分符表有相同的就是
if(!strcmp(character, doubleword[i]))
return true ;
}
return false ;
}
//判斷是否是 '/' 用於判斷是否有可能是註釋的開始標誌
bool isDiagonal(char temp) {
if(temp=='/' || temp=='*') //註釋的開始標誌由 /* 組成
return true ;
return false ;
}
//輸出之前還未輸出的內容
void printbefore() {
/*各種長度不爲0則說明在正在進行詞法分析的字符之前有不同類型的東西沒有輸出
由於這些肯定是在當前字符之前的所以應當優先輸出*/
if(identifierLength != 0) { //之前是一個標識符或者保留字
char temp[50] ;
strcpy(temp, identifier) ;
strlwr(temp) ; //防止區分大小寫,將所有大寫都換成小寫
if(isNumber(identifier[0])) { //非法標識符錯誤,第一位是數字
sprintf(errors[errorSum], "第%d行: 非法標識符%s", line, identifier);
errorSum++ ;
}
else if(binarySearch(temp, 0, keywordNum-1)) { //保留字
printf("%s %s\n", keyword[keywordPlace], identifier) ;
fprintf(outputFile, "%s %s\n", keyword[keywordPlace], identifier) ;//寫入輸出文件中
}
else { //標識符
printf("ID %s\n", identifier) ;
fprintf(outputFile, "ID %s\n", identifier) ;
}
memset(identifier, 0, sizeof(identifier)) ; //清空字符串內容
identifierLength = 0 ; //清空字符串長度
}
if(numbersLength != 0) { //之前是一個數字
int num = 0 ;
for(int i=0; i<numbersLength; i++) { //將字符串變爲整型
num *= 10 ;
num += (numbers[i] - '0') ;
}
printf("NUM %d\n", num) ;
fprintf(outputFile, "NUM %d\n", num) ;
memset(numbers, 0, sizeof(numbers)) ; //清空字符串內容
numbersLength = 0 ;
}
if(characterNum != 0) { //之前是單分符或雙分符
if(characterNum > 2) { //沒有大於兩個字符的組合,屬於錯誤
sprintf(errors[errorSum], "第%d行: 非法標識符%s", line, character);
errorSum++ ;
}
if(characterNum == 1) { //單分符
printf("%c %c\n", character[0], character[0]) ;
fprintf(outputFile, "%c %c\n", character[0], character[0]) ;
}
if(characterNum == 2) { //雙分符
if(isDoubleword()) { //首先要判斷這兩個連續的字符是不是雙分符
printf("%s %s\n", character, character) ;
fprintf(outputFile, "%s %s\n", character, character) ;
}
}
memset(character, 0, sizeof(character)) ; //不管是單分符還是雙分符都要清空
characterNum = 0 ;
}
if(noteNum != 0) { //之前是單分符或註釋
if(noteNum == 1) { //單分符
printf("%c %c\n", note[0], note[0]) ;
fprintf(outputFile, "%c %c\n", note[0], note[0]) ;
memset(note, 0, sizeof(note)) ; //不管是單分符還是註釋都要清空
noteNum = 0 ;
}
else if(noteNum == 2) { //註釋
if(!strcmp(note, "/*")) //多行註釋開始
startFlag = true ;
else {
printf("%c %c\n", note[0], note[0]) ;
printf("%c %c\n", note[1], note[1]) ;
fprintf(outputFile, "%c %c\n%c %c\n", note[0], note[0], note[1], note[1]) ;
}
memset(note, 0, sizeof(note)) ;
noteNum = 0 ;
}
}
}
//詞法分析
void wordanalysis(char temp) {
if(isNull(temp)) { //遇到空字就繼續看下一個字符
printbefore() ; //如果空字前有東西沒有處理需要進行操作
prior = temp ; //進入到此處就不會執行後面的內容了,此處也要設置上一個字符
return ; //退出該函數
}
if(isLetter(temp) || (isLetter(prior)&&isNumber(temp))) { //判斷當前字符是不是字母
//字母加數字的組合也可以是標識符
if(isNumber(prior) && identifierLength==0 ) { //但是數字加字母是非法的標識符
//如果標識符數組中還沒有任何字符,但是在第一個字母之前有任意多個數字則是非法標識符
for(int i=0; i<numbersLength; i++) { //將常數數組中的數組全部轉移到標識符數組中
identifier[identifierLength] = numbers[i] ;
identifierLength++ ;
}
memset(numbers, 0, sizeof(numbers)) ; //清空常數內容
numbersLength = 0 ;
}
if(!isLetter(prior) && identifierLength==0)//標識符長度爲0才證明之前的數字不包含在標識符中
printbefore() ;
if(!startFlag) { //可能在之前的if中判斷出進入註釋了就不能再繼續了
identifier[identifierLength] = temp ; //是就將其加入到當前的標識符單詞中
identifierLength++ ;
}
}
else if(isNumber(temp)) { //判斷當前字符是不是數字
if(!isNumber(prior)) //如果前後字符類型不同就有可能要輸出一些內容
printbefore() ;
if(!startFlag) {
numbers[numbersLength] = temp ; //是數字就加入到數字數組中
numbersLength++ ;
}
}
else if(isPureSingleword(temp)) { //判斷當前字符是不是純單分符
if(!isPureSingleword(prior))
printbefore();
if(!startFlag) {
printf("%c %c\n", temp, temp) ; //是純單分符就直接輸出
fprintf(outputFile, "%c %c\n", temp, temp) ;
}
}
else if(!(temp=='*'&&prior=='/') && isSingleword(temp) ) { //判斷當前字符是不是單分符
//要注意考慮註釋的情況
if(!isSingleword(prior))
printbefore() ;
if(!startFlag) {
character[characterNum] = temp ;
characterNum++ ;
}
}
else if(isDiagonal(temp)) { //判斷當前字符是不是/
if(!isDiagonal(prior))
printbefore() ;
if(!startFlag) {
note[noteNum] = temp ;
noteNum++ ;
if(!strcmp(note, "/*")) { //多行註釋開始
startFlag = true ;
memset(note, 0, sizeof(note)) ;
noteNum = 0 ;
}
}
}
else { //錯誤判斷非法字符
printbefore() ;
if(!startFlag) {
sprintf(errors[errorSum], "第%d行: 非法字符%c", line, temp);
errorSum++ ;
}
}
prior = temp ; //處理完當前字符後就將其設爲上一個字符
}
int main() {
//初始化,得到輸入輸出文件
printf("請輸入 輸入文件地址:") ;
scanf("%s", inputAddress) ;
if((inputFile=fopen(inputAddress, "r")) == NULL) //讀取輸入文件
printf("\nerror: 無法打開輸入文件\n\n");
printf("請輸入 輸出文件地址:") ;
scanf("%s", outputAddress) ;
if((outputFile=fopen(outputAddress, "w")) == NULL) //讀取輸出文件
printf("\nerror: 無法打開結果文件\n\n");
while(!feof(inputFile)) { //讀取輸入文件直到內容結束
char current ; //當前字符
fscanf(inputFile, "%c", ¤t) ; //每次讀取一個字符
if(startFlag) { //在註釋中就不進行詞法分析
if(current=='/' && prior=='*') { //當遇到註釋結束標誌時修改狀態
prior = current ;
startFlag = false ;
}
else {
if(current=='\n') //註釋中的行數也要考慮在內
line++ ;
prior = current ; //註釋中只需要記錄上一個字符是什麼,爲了找到結束註釋做準備
}
}
else //不在註釋中就要進行詞法分析
wordanalysis(current) ;
current = ' ' ; //每次處理完都要設爲空字防止末尾字符多輸出一次
}
if(startFlag) { //如果註釋沒有結束符號,則註釋之後的內容都沒有了,可能會出錯
//printf("\nerror: 註釋沒有封閉\n\n");
sprintf(errors[errorSum], "第%d行: 註釋沒有封閉", line);
errorSum++ ;
}
//輸出分析結果
printf("\n\n===========================================") ;
if(errorSum == 0)
printf("\n詞法分析成功\n") ;
else {
printf("\n錯誤數量: %d \n", errorSum) ;
for(int i=0; i<errorSum; i++)
printf("%s\n", errors[i]) ;
}
return 0 ;
}
5 總結
- 本來在做這個詞法分析的時候,課本最後是有完整代碼的,但是我全部做完了之後才發現,所以我的這個詞法分析是完全由我自己想出來的野路子。用到現在爲止感覺還好
- 打代碼之前一定要先進行分析!這次雖然我做的是野路子,但就是先看了課本上的這個程序流程圖就有了大致的思路,打代碼的過程就很輕鬆
- 前段時間學習了編譯原理,實驗課要求做一個TEST語言的編譯器,我會根據不同階段的內容出一個系列,這是這個系列的第一篇