天天看点

编译原理词法分析器的c++实现一、题目的理解和说明二、程序功能和框架三、设计说明四、测试数据与运行结果五、总结

一、题目的理解和说明

编译原理这门课是计算机专业的核心课程之一,是一门研究软件是什么,为什么可以运行,以及怎么运行的学科。编译系统的改进将会直接对其上层的应用程序的执行效率,执行原理产生深刻的影响。编译原理的目的是将源语言翻译成目标语言。其过程分为词法分析、语法分析、语义分析中间代码生成、中间代码优化、代码生成等五个阶段。而本次实验所解决的问题是编译程序的第一个阶段–词法分析。

实验要求实现一个可以分析一门编程语言的语法分析器。我以c/c++语言的语法为例,采用c/c++进行编程实现一个可以对c/c++语言进行词法分析的分析器。

该词法分析器的目标为:处理c语言源程序,过滤掉无用符号,判断源程序中单词的合法性,并分解出正确的单词,以二元组形式存放在文件中。

词法分析器所做的工作主要是对源程序进行编译预处理(去除注释、无用的回车换行找到包含的文件等)之后,对整个源程序进行分解,分解成一个个单词,这些单词有且只有五类,分别是标识符、保留字、常数、运算符、界符。

词法分析面向的对象是单个的字符,目的是把它们组成有效的单词(字符串);而语法的分析则是利用词法分析的结果作为输入来分析是否符合语法规则并且进行语法制导下的语义分析,最后产生四元组(中间代码),进行优化(可有可无)之后最终生成目标代码。因此词法分析是整个编译程序的基础,如果词法分析做不好,那么会严重英雄后续编译阶段的效果。二

二、程序功能和框架

1. 识别单词的类

总共需要识别的单词分为五类:标识符、保留字、常数、运算符、界符。为了能更大限度的处理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,就唯一确定了‘}’这个单词。

2.程序框架设计

确定好这些单词种类后,然后程序需要实现以下几个功能函数:

保留字识别函数:

int SearchRWord(char RW[][20],char s[])

字母判别函数:

bool IsLetter(char letter)

数字判别函数:

bool IsDigit(char digit)

预处理程序:

void PreProcessing(char r[], int pProject)

扫描器(算法核心):

void Scanner(int &syn, char resourceProject[], char token[], int &pProject)

上述函数中,最核心的在于扫描器。扫描器的实现主要依据于DFA理论。旨在实现对读入且经过预处理后的字符流中逐个字符进行扫描判别,然后以实现的有限自动机算法逐个识别以空格隔开的单词,并通过查找其相关二元组序列:(自身值,种别码)来达成识别单词种类的目的。

程序的基本处理流程描述为:

(1)词法分析程序打开源文件,读取文件内容,直至遇上’$’文件结束符,然后读取结束。

(2)对读取的文件进行预处理,从头到尾进行扫描,去除//和的内容,以及一些无用的、影响程序执行的符号如换行符、回车符、制表符等。但是千万注意不要在这个时候去除空格,因为空格在词法分析中有用,比如说int i=3;这个语句,如果去除空格就变成了“inti=3”,这样就失去了程序的本意,因此不能在这个时候去除空格。

(3)选下面就要对源文件从头到尾进行扫描了,从头开始扫描,这个时候扫描程序首先要询问当前的字符是不是空格,若是空格,则继续扫描下一个字符,直至不是空格,然后询问这个字符是不是字母,若是则进行标识符和保留字的识别;若这个字符为数字,则进行数字的判断。否则,依次对这个字符可能的情况进行判断,若是将所有可能都走了一遍还是没有知道它是谁,则认定为错误符号,输出该错误符号,然后结束。每次成功识别了一个单词后,单词都会存在token[ ]中。然后确定这个单词的种别码,最后进行下一个单词的识别。这就是扫描程序进行的工作,可以说这个程序彻底实现了确定有限自动机的某些功能,比如说识别标识符,识别数字等。为了简单起见,这里的数字只是整数。

(4)主控程序主要负责对每次识别的种别码syn进行判断,对于不同的单词种别做出不同的反应,如对于标识符则将其插入标识符表中。对于保留字则输出该保留字的种别码和助记符,等等吧。直至遇到syn=0;程序结束。

三、设计说明

1.变量存储

在报告的第二部分,我介绍了单词的五种种类:标识符、保留字、常数、运算符、界符。

在程序实现中,我设置以下数据结构分别存储上述五种单词种类:

/*******保留字表*******/
static char RWord[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 OandD[36][10] = {	
"+", "-", "*", "/", "<", "<=",	
">", ">=", "=", "==","!=", ";",	
"(", ")", "^", ",", "\"", "\'",	
"#", "&","&&", "|", "||", "%",	
"~", "<<", ">>", "[", "]", "{",	
"}", "\\", ".", "\?", ":", "!" };
/*******界符与运算符表*******/
           
/*******标识符表*******/
static char IDtable[1000][50] = { "" };//初始为空
/*******标识符表*******/
           

2. 基本函数的实现

/*******识别保留字*******/
int SearchRWord(char RW[][20],char s[]) {
 for (int i = 0;i<32;i++) {
  if (strcmp(RW[i], s) == 0) {
   //所识别的单词和保留字表中;
   //存储的保留字比较;
   //正确则返回其种别码;
   return i + 1;
  }
 }
 //不匹配则返回-1
 //即:该单词可能是标识符或者错别字
 return -1;
}
/*******识别保留字*******/
           
/*******字母判别*******/
bool IsLetter(char letter){
 //C/c++语言中允许下划线也为标识符的一部分可以放在首部或其他地方
 if (letter >= 'a'&&letter <= 'z' || letter >= 'A'&&
 letter <= 'Z' || letter == '_')
  return true;
 else
  return false;
}
/*******字母判别*******/
           
/*******数字判别*******/
bool IsDigit(char digit){
 if (digit >= '0'&&digit <= '9')
  return true;
 else
  return false;
}
/*******数字判别*******/
           
/*******预处理程序,去除错误字符和注释*******/
void PreProcessing(char r[], int pProject){
 char tempString[10000];
 int count = 0;
 for (int i = 0; i <= pProject; i++){
  if (r[i] == '/'&&r[i + 1] == '/'){
   //若为单行注释“//”,则去除注释后面的东西,直至遇到回车换行
   while (r[i] != '\n'){
    i++;//向后扫描
   }
  }
  if (r[i] == '/'&&r[i + 1] == '*'){
   //若为多行注释“/*......*/”则去除该内容
   i += 2;
   while (r[i] != '*' || r[i + 1] != '/'){
    i++;//继续扫描
    if (r[i] == '$'){
     printf("注释出错,没有找到 */,程序结束!!!\n");
     exit(0);
    }
   }
   i += 2;//跨过“*/”
  }
  if (r[i] != '\n'&&r[i] != '\t'&&r[i] != '\v'&&r[i] != '\r'){
   //若出现无用字符,则过滤;否则加载
   tempString[count++] = r[i];
  }
 }
 tempString[count] = '\0';
 strcpy(r, tempString);//产生净化之后的源程序,将处理后的源程序字符串重新返回
}
/*******预处理程序,去除错误字符和注释*******/
           

预处理的过程需要实现对输入的字符流进行筛选,把注释、换行符、无效字符、错误字符等进行净化删除得到只有空格的程序字符流。注意:空格是区别五类单词的重要分解符号,因此不可以净化。

3.扫描器的实现

扫描器作为词法分析的核心函数,其主要功能是实现净化后的字符流中每个单词的分类,并生成单词的对应二元组写到文件中。主要理论是使用DFA理论进行构建。

/*******分析模块,词法分析器的核心*******/
//该模块主要的原理支撑是DFA的状态转换图的设计
void Scanner(int &syn, char resourceProject[], char token[], int &pProject){
 int i, count = 0;//count用来做token[]的指示器,收集有用字符
 char ch;//作为判断使用
 ch = resourceProject[pProject];
 while (ch == ' '){//过滤空格,防止程序因识别不了空格而结束
  pProject++;
  ch = resourceProject[pProject];
 }
 for (i = 0; i < 20; i++){//每次收集前先清零
  token[i] = '\0';
 }
 if (IsLetter(resourceProject[pProject])){//开头为字母
  token[count++] = resourceProject[pProject];//收集
  pProject++;//下移
  while (IsLetter(resourceProject[pProject]) || IsDigit(resourceProject[pProject])){//后跟字母或数字
   token[count++] = resourceProject[pProject];//收集
   pProject++;//下移
  }//多读了一个字符既是下次将要开始的指针位置
  token[count] = '\0';
  syn = SearchRWord(RWord, token);//查表找到种别码
  if (syn == -1){//若不是保留字则是标识符
   syn = 100;//标识符种别码
  }
  return;
 }
 else if (IsDigit(resourceProject[pProject])){//首字符为数字
  while (IsDigit(resourceProject[pProject])){//后跟数字
   token[count++] = resourceProject[pProject];//收集
   pProject++;
  }//多读了一个字符既是下次将要开始的指针位置
  token[count] = '\0';
  syn = 99;//常数种别码
 }
 else if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == ';' || ch == '(' || ch == ')' || ch == '^'
  || ch == ',' || ch == '\"' || ch == '\'' || ch == '~' || ch == '#' || ch == '%' || ch == '['
  || ch == ']' || ch == '{' || ch == '}' || ch == '\\' || ch == '.' || ch == '\?' || ch == ':'){
  //若为运算符或者界符,查表得到结果
  token[0] = resourceProject[pProject];
  token[1] = '\0';//形成单字符串
  for (i = 0; i < 36; i++)
  {//查运算符界符表
   if (strcmp(token, OandD[i]) == 0){
    syn = 33 + i;//获得种别码,使用了一点技巧,使之呈线性映射
    break;//查到即推出
   }
  }
  pProject++;//指针下移,为下一扫描做准备
  return;
 }
 else  if (resourceProject[pProject] == '<'){//<,<=,<<
  pProject++;//后移,超前搜索
  if (resourceProject[pProject] == '='){
   syn = 38;
  }
  else if (resourceProject[pProject] == '<'){//左移
   pProject--;
   syn = 58;
  }
  else{
   pProject--;
   syn = 37;
  }
  pProject++;//指针下移
  return;
 }
 else  if (resourceProject[pProject] == '>'){//>,>=,>>
  pProject++;
  if (resourceProject[pProject] == '=')
   syn = 40;
  else if (resourceProject[pProject] == '>')
   syn = 59;
  else{
   pProject--;
   syn = 39;
  }
  pProject++;
  return;
 }
 else  if (resourceProject[pProject] == '='){//=.==
  pProject++;
  if (resourceProject[pProject] == '=')
   syn = 42;
  else{
   pProject--;
   syn = 41;
  }
  pProject++;
  return;
 }
 else  if (resourceProject[pProject] == '!'){//!,!=
  pProject++;
  if (resourceProject[pProject] == '=')
   syn = 43;
  else{
   syn = 68;
   pProject--;
  }
  pProject++;
  return;
 }
 else  if (resourceProject[pProject] == '&'){//&,&&
  pProject++;
  if (resourceProject[pProject] == '&')
   syn = 53;
  else{
   pProject--;
   syn = 52;
  }
  pProject++;
  return;
 }
 else  if (resourceProject[pProject] == '|'){//|,||
 pProject++;
 if (resourceProject[pProject] == '|')
  syn = 55;
 else{
  pProject--;
  syn = 54;
 }
 pProject++;
 return;
 }
 else  if (resourceProject[pProject] == '$')//结束符
  syn = 0;//种别码为0
 else{//不能被以上词法分析识别,则出错。
  printf("error:there is no exist %c \n", ch);
  exit(0);
 }
}
/*******分析模块,词法分析器的核心*******/
           

其主要处理流程为:首先从头开始扫描,这个时候扫描程序首先要询问当前的字符是不是空格,若是空格,则继续扫描下一个字符,直至不是空格,然后询问这个字符是不是字母,若是则进行标识符和保留字的识别;若这个字符为数字,则进行数字的判断。否则,依次对这个字符可能的情况进行判断,若是将所有可能都走了一遍还是没有知道它是谁,则认定为错误符号,输出该错误符号,然后结束。每次成功识别了一个单词后,单词都会存在token[ ]中。然后确定这个单词的种别码,最后进行下一个单词的识别。这就是扫描程序进行的工作,可以说这个程序彻底实现了确定有限自动机的某些功能,比如说识别标识符,识别数字等。为了简单起见,这里的数字只是整数。

4.主函数的操作

主函数中主要是对必要变量的初始化操作以及对文件的读写操作。设置resourceProject变量存储从txt文本读入的字符流,为了方便起见,我只设置读入的字符流最大值为10000个char类型的字符。设置token变量为了存储每次识别的单词以便于找到其相对应种别码。syn是对当前单词种别码的保存,我设置’ ’ 为 读 入 的 t x t 文 本 的 扫 描 终 点 标 识 符 , 其 s y n 值 为 0 , 当 读 入 ’ ’为读入的txt文本的扫描终点标识符,其syn值为0,当读入’ ’为读入的txt文本的扫描终点标识符,其syn值为0,当读入’’意味着扫描的字符流到达终点,即当syn==0时,退出扫描程序,扫描结束,词法分析结束。pProject变量是源程序指针,其始终指向当前识别的字符位置。主函数操作流程为:先打开预设定的txt文本读入其中全部字符到resourceProject变量中,然后调用预处理程序得到净化后的字符流,覆盖存储到resourceProject变量中。然后调用扫描器识别各个单词,此时初始时syn=-1,pProject=0。处理完后把处理结果存储到一个txt文件中并输出即可。

int main(){
 //打开一个文件,读取其中的源程序
 char resourceProject[10000];
 char token[20] = { 0 };
 int syn = -1, i;//初始化
 int pProject = 0;//源程序指针
 FILE *fp, *fp1;
 if ((fp = fopen("F:\\大三下课程\\编译原理(必修)\\词法分析器\\zyr_rc.txt", "r")) == NULL){//打开源程序
  cout << "can't open this file";
  exit(0);
 }
 resourceProject[pProject] = fgetc(fp);
 while (resourceProject[pProject] != '$'){//将源程序读入resourceProject[]数组
  pProject++;
  resourceProject[pProject] = fgetc(fp);
 }
 resourceProject[++pProject] = '\0';
 fclose(fp);
 cout << endl << "源程序为:" << endl;
 cout << resourceProject << endl;
 //对源程序进行过滤
 PreProcessing(resourceProject, pProject);
 cout << endl << "过滤之后的程序:" << endl;
 cout << resourceProject << endl;
 pProject = 0;//从头开始读
 if ((fp1 = fopen("F:\\大三下课程\\编译原理(必修)\\词法分析器\\zyr_compile.txt", "w+")) == NULL){//打开源程序
  cout << "can't open this file";
  exit(0);
 }//F:\\大三下课程\\编译原理(必修)\\词法分析器\\zyr_compile.txt
 while (syn != 0){
  //启动扫描
  Scanner(syn, resourceProject, token, pProject);
  if (syn == 100){//标识符
   for (i = 0; i < 1000; i++){//插入标识符表中
    if (strcmp(IDtable[i], token) == 0)//已在表中
     break;  
    if (strcmp(IDtable[i], "") == 0){//查找空间
     strcpy(IDtable[i], token);
     break;
    }
   }//F:\大三下课程\编译原理(必修)\词法分析器\zyr_rc.txt
   printf("(标识符  ,%s)\n", token);
   fprintf(fp1, "(标识符   ,%s)\n", token);
  }
  else if (syn >= 1 && syn <= 32){//保留字
   printf("(%s   ,  --)\n", RWord[syn - 1]);
   fprintf(fp1, "(%s   ,  --)\n", RWord[syn - 1]);
  }
  else if (syn == 99){//const 常数
   printf("(常数   ,   %s)\n", token);
   fprintf(fp1, "(常数   ,   %s)\n", token);
  }
  else if (syn >= 33 && syn <= 68){
   printf("(%s   ,   --)\n", OandD[syn - 33]);
   fprintf(fp1, "(%s   ,   --)\n", OandD[syn - 33]);
  }
 }
 for (i = 0; i < 100; i++){//插入标识符表中
  if (strcmp(IDtable[i],"")==0)
   break;
  printf("第%d个标识符:  %s\n", i + 1, IDtable[i]);
  fprintf(fp1, "第%d个标识符:  %s\n", i + 1, IDtable[i]);
 }
 fclose(fp1);
 return 0;
}
           

四、测试数据与运行结果

测试内容:

文件路径及其文件名:

F:\\大三下课程\\编译原理(必修)\\词法分析器\\zyr_rc.txt

文件内容:

int main(){
 //打开一个文件,读取其中的源程序
 char resourceProject[10000];
 char token[20] = { 0 };
 int syn = -1, i;//初始化
 int pProject = 0;//源程序指针
 FILE *fp, *fp1;
 if ((fp = fopen("F:\\大三下课程\\编译原理(必修)\\词法分析器\\zyr_rc.txt", "r")) == NULL){//打开源程序
  cout << "can't open this file";
  exit(0);
 }
 resourceProject[pProject] = fgetc(fp);
 while (resourceProject[pProject] != '$'){//将源程序读入resourceProject[]数组
  pProject++;
  resourceProject[pProject] = fgetc(fp);
 }
 resourceProject[++pProject] = '\0';
 fclose(fp);
 cout << endl << "源程序为:" << endl;
 cout << resourceProject << endl;
 //对源程序进行过滤
 PreProcessing(resourceProject, pProject);
 cout << endl << "过滤之后的程序:" << endl;
 cout << resourceProject << endl;
 pProject = 0;//从头开始读
 if ((fp1 = fopen("F:\\大三下课程\\编译原理(必修)\\词法分析器\\zyr_compile.txt", "w+")) == NULL){//打开源程序
  cout << "can't open this file";
  exit(0);
 }
 $
 
           

输出结果存储文件路径及其文件名:

F:\\大三下课程\\编译原理(必修)\\词法分析器\\zyr_compile.txt

运行结果为:

(int , --)

(标识符 ,main)

(( , --)

() , --)

({ , --)

(char , --)

(标识符 ,resourceProject)

([ , --)

(常数 , 10000)

(] , --)

(; , --)

(char , --)

(标识符 ,token)

([ , --)

(常数 , 20)

(] , --)

(= , --)

({ , --)

(常数 , 0)

(} , --)

(; , --)

(int , --)

(标识符 ,syn)

(= , --)

(- , --)

(常数 , 1)

(, , --)

(标识符 ,i)

(; , --)

(int , --)

(标识符 ,pProject)

(= , --)

(常数 , 0)

(; , --)

(标识符 ,FILE)

(* , --)

(标识符 ,fp)

(, , --)

(* , --)

(标识符 ,fp1)

(; , --)

(if , --)

(( , --)

(( , --)

(标识符 ,fp)

(= , --)

(标识符 ,fopen)

(( , --)

(" , --)

(标识符 ,D)

(: , --)

(\ , --)

(\ , --)

(标识符 ,zyr_rc)

(. , --)

(标识符 ,txt)

(" , --)

(, , --)

(" , --)

(标识符 ,r)

(" , --)

() , --)

() , --)

(== , --)

(标识符 ,NULL)

() , --)

({ , --)

(标识符 ,cout)

(<< , --)

(< , --)

(" , --)

(标识符 ,can)

(’ , --)

(标识符 ,t)

(标识符 ,open)

(标识符 ,this)

(标识符 ,file)

(" , --)

(; , --)

(标识符 ,exit)

(( , --)

(常数 , 0)

() , --)

(; , --)

(} , --)

(标识符 ,resourceProject)

([ , --)

(标识符 ,pProject)

(] , --)

(= , --)

(标识符 ,fgetc)

(( , --)

(标识符 ,fp)

() , --)

(; , --)

(while , --)

(( , --)

(标识符 ,resourceProject)

([ , --)

(标识符 ,pProject)

(] , --)

(!= , --)

(’ , --)

第1个标识符: main

第2个标识符: resourceProject

第3个标识符: token

第4个标识符: syn

第5个标识符: i

第6个标识符: pProject

第7个标识符: FILE

第8个标识符: fp

第9个标识符: fp1

第10个标识符: fopen

第11个标识符: D

第12个标识符: zyr_rc

第13个标识符: txt

第14个标识符: r

第15个标识符: NULL

第16个标识符: cout

第17个标识符: can

第18个标识符: t

第19个标识符: open

第20个标识符: this

第21个标识符: file

第22个标识符: exit

第23个标识符: fgetc

五、总结

这次的实验,算法部分并不难,只要对DFA有一定熟练的掌握,扫描器模块很好写。比较麻烦的是五种类型的字符个数越多程序就越长。但为了能识别大部分程序,我依然选用了比较大的子集,下了一定的功夫,最终这个词法分析器的处理能力还是可以处理大多数c/c++程序的。同时加深了我对字符的认识。程序的可读性还算不错。我的程序没有实现的是对所有复合运算的分离,但原理是相同的,比如“+=“,只需在”+“的逻辑之后向前扫描就行了,因此就没有再加上了。感受最深的是学习编译原理必须要做实验,写程序,这样才会提高自己的动手能力,加深自己对难点的理解,对于以后的求first{},follow{},fisrtVT{},lastVT{}更是应该如此。基于此次实验,让我对编译程序有了基础性的认识,对后面的语法分析的实验也有了一定想法。为之后更好的学习和实践做了充分准备。

继续阅读