【编译原理】【C语言】实验一:手动构造词法分析器


C语言
实验环境:Visual Studio 2019
author:zoxiii


1、实验内容

  写出完整词法分析器程序,并用一段程序语句进行调试、运行。

2、前期准备

2.1 待分析的C语言子集的词法

单词大概可分为的5类:标识符、保留字、常数、运算符、界符。

  1. 标识符:以字母开头的包含字母和数字的符号
  2. 保留字:while、if、else、switch、case
  3. 常数:数字0~9
  4. 运算符:+、-、*、<、<=、==、=
  5. 界符:;
  6. 需略除的:空格、制表符、换行符

2.2 C语言子集的单词符号表示表

  首先根据我们要构造的C语言子集的简单词法分析器,列出所要实现的单词符号以及它们的种别编码和内码值。
  为了方便记忆,还添加了助记符来表示单词符号。具体如下表:

单词符号种别编码助记符内码值
while1while
if2if
else3else
switch4switch
case5case
标识符6idid在符号表中的位置
常数7numnum在常数表中的位置
+8+
-9-
*10*
<=11relopLE
<11relopLT
==11relopEQ
=12=
;13;

2.3 C语言子集对应的状态转换图

  根据需要构造的词法分析器的单词符号,设计了对应的状态转换图。其中,首先对输入串预处理,去除空格、制表符等,再根据单子符号表得到状态转换图,并分析每种状态应输出的单词二元组信息。

在这里插入图片描述

图1-C语言子集对应的状态转换图

3、分析过程

3.1 词法分析子程序

  对于前期分析要实现的词法分析器需引入相应的变量和函数:
  变量如下:
(1) token:用来存放构成单词符号的字符串;
(2) ch:用来存放每一次词法分析获取的字符;
(3) ptr_token:单词缓冲区指针;
  函数如下:
(1) void Getchar():将下一输入字符读到ch中,扫描指针前移一字符位置;
(2) void getbe():检查ch中的字符是否为空白。若是,则调用Getchar直至ch中为非空字符为止 ;
(3) void concatenation():将ch中的字符连接到token之后作为新的字符串;
(4) bool letter():判断ch中的字符是否为字母,是则返回true(1),不是则返回false(0);
(5) bool digit():判断ch中的字符是否为数字,是则返回true(1),不是则返回false(0);
(6) int reserve():按token中的字符串查找保留字表,若它是一个保留字则返回它的编码,否则返回0值(假定0不是保留字的编码);
(7) void retract():将扫描指针回退一个字符位置,将ch置为空白字符;
(8) void buildlist():将标识符登录到符号表中或将常数登录到常数表中;
(9) void error():出现非法字符,词法分析出错,显示出错的字符;
(10) void LexicalAnalysis():词法分析过程函数;
  扫描子程序的主要流程图如下图2所示:
在这里插入图片描述

图2-词法分析子程序流程图

3.2 主程序

  主程序流程较为简单,如下图图3所示。但在之前需要在程序开始时定义一些全局变量,如关键字数组、需分析的语句数组、空的标识符表数组、常数表数组等以及相应的数组指针,便于后面的词法分析。
在这里插入图片描述

图3-主程序流程图

3.3 源代码

#include<iostream>
#include<string.h>
using namespace std;
//声明数组变量+指针
char input[] = "Hello World;I am XXX;x=5;y=9;if x<y	x=y;else   x=x; case 7  x>7;";  //需要分析的词句
const char *keyword[] = {"while","if","else","switch","case"};//关键字
int keyword_length=sizeof(keyword)/sizeof(char*);
int ptr_input = 0; //测试程序数组指针
int ptr_token;     //读取到的字符数组指针
char ch;         //字符变量,存放最新读入的源程序字符
char token[255] = "";  //字符数组,存放构成单词符号的字符串
//声明buildlist的相关变量+指针
int num_table[50] = { 0 }; //常数表,存放构成的常数
int ptr_num=0;              //常数表指针
char id_table[255][255] = {0};//标识符表,存放构成的标识符
int ptr_id=0;                           //标识符表指针
int ptr_present;                        //当前指针
//声明函数定义 
void Getchar();//将下一输入字符读到ch中,扫描指针前移一字符位置
void getbe();//检查ch中的字符是否为空白。若是,则调用Getchar直至ch中为非空字符为止 
void concatenation();//将ch中的字符连接到token之后作为新的字符串
bool letter();//判断ch中的字符是否为字母,是则返回true(1),不是则返回false(0)
bool digit();//判断ch中的字符是否为数字,是则返回true(1),不是则返回false(0)
int reserve();//按token中的字符串查找保留字表,若它是一个保留字则返回它的编码,否则返回0值(假定0不是保留字的编码)
void retract();//将扫描指针回退一个字符位置,将ch置为空白字符
void buildlist();//将标识符登录到符号表中或将常数登录到常数表中
void error();//出现非法字符,词法分析出错,显示错误信息
void LexicalAnalysis();//词法分析过程函数
/**
 * 函数功能:从输入缓冲区下一输入字符读到ch中,并将扫描指针前移一字符位置。
 */
void Getchar()
{
	ch = input[ptr_input];
	ptr_input++;
} 
/**
 * 函数功能:检查ch中的字符是否为空白;若是,则调用Getchar直至ch中为非空字符为止。
 */
void getbe()
{
	while (ch == ' '||ch == '\t')
	        Getchar();
} 
/**
 * 函数功能:将ch中的字符连接到token之后作为新的字符串
 */
void concatenation()
{
	token[ptr_token] = ch;
	ptr_token++;
	token[ptr_token] = '\0';
} 
/**
 * 函数功能:判断ch中的字符是否为字母,是则返回true(1),不是则返回false(0)
 */
bool letter()
{
	if (ch >= 'a'&&ch <= 'z' || ch >= 'A'&&ch <= 'Z')
		return 1;
	else
		return 0;
}
/**
 * 函数功能:判断ch中的字符是否为数字,是则返回true(1),不是则返回false(0)
 */
bool digit()
{
	if (ch >= '0'&&ch <= '9')
		return 1;
	else
		return 0;
} 
/**
 * 函数功能:按token中的字符串查找保留字表,若它是一个保留字则返回它的编码,否则返回0值(假定0不是保留字的编码)
 */
int reserve()
{
	int i = 0;
	while(i < keyword_length)
	{
		if (!strcmp(keyword[i], token))
			return i + 1;   //如果相等,则返回该关键字的种别编码
		i++;
	}
	return 0;       //如果不是关键字,则返回0
} 
/**
 * 函数功能:将扫描指针回退一个字符位置,将ch置为空白字符
 */
void retract()
{
	ptr_input--;
}
/**
 * 函数功能:出现非法字符,词法分析出错,显示错误信息
 */
void error()
{
        cout<<"Illegal character:"<<ch<<endl;
}
/**
 * 函数功能:将标识符登录到符号表中或将常数登录到常数表中
 */    
void buildlist()
{ 
        char ch_head=token[0]-'0';
        if(ch_head >= 0 && ch_head <= 9)
        {
                int j=0;
                while(j < ptr_num)
                {
                        if(atoi(token)==num_table[j])
                        {
                                ptr_present=j;
                                break;
                        }
                        j++;
                }
                if(j==ptr_num)
                {
                        num_table[ptr_num]=atoi(token);
                        ptr_present=ptr_num;
                        ptr_num++;
                }
        }
        else
        {
                int i = 0;
	        while(i < ptr_id)
	        {
		        if (!strcmp(id_table[i], token))
                        {
                                ptr_present = i;
			        break;
                        }
		        i++;
	        }
                if(i==ptr_id)
                {
                        strcpy(id_table[ptr_id],token);
                        ptr_present=ptr_id;
                        ptr_id++;       
                }
        }
}
/**
 * 函数功能:词法分析过程函数
 */
void LexicalAnalysis()                  
{         
        ptr_token = 0;   //单词缓冲区指针
        Getchar();       //获取当前字符到ch中
        getbe();         //滤除空格
        if(letter())            //首字符为字母
        {
                while (letter() || digit ())
                {
                        concatenation();        //将当前读入的字符送入token数组
                        Getchar();              //获取下个字符
                }
                retract();    //扫描指针回退一个字符
                int index = reserve();//查询是否为关键字
                if (index==0)                   
                {

                        buildlist();//将标识符登录到符号表中
                        cout<<"(\t"<<"id"<<"\t,\t"<<ptr_present<<"\t)\tid_table["<<ptr_present<<"]="<<token<<"\n";//是标识符
                        //return (id,指向id的符号表入口指针);
                }
                else
                cout<<"(\t"<<keyword[index-1]<<"\t,\t"<<" "<<"\t)\n";//是关键字
                //return (关键字码, null);
        }
	else if (digit())  //首字符是数字
	{
                while (digit())
                {
                concatenation();
                Getchar();
                }
                retract();
                buildlist(); /*将常数登录到常数表中*/
                cout<<"(\t"<<"num"<<"\t,\t"<<ptr_present<<"\t)\tnum_table["<<ptr_present<<"]="<<token<<"\n";//是常数
                //return (num, num的常数表入口指针);
        }
        else switch (ch)
        {
                case '+':
                        cout<<"(\t"<<"'+'"<<"\t,\t"<<" "<<"\t)\n";//是"+"
                        //return ('+', null);
                        break;
                case '?':
                        cout<<"(\t  "<<"'-'"<<"\t,\t"<<" "<<"\t)\n";//是"-"
                        //return ('?', null);
                        break;
                case '*':
                        cout<<"(\t"<<"'*'"<<"\t,\t"<<" "<<"\t)\n";//是"*"
                        //return ('*', null);
                        break;
                case '<':
                        Getchar();
                        if (ch == '=')
                                cout<<"(\t"<<"relop"<<"\t,\t"<<"LE"<<"\t)\n";//是"<="
                                //return (relop,LE);
                        else
                        {
                                retract();
                                cout<<"(\t"<<"relop"<<"\t,\t"<<"LT"<<"\t)\n";//是"<"
                                //return (relop,LT);
                        }
                        break;
                case '=':
                        Getchar();
                        if (ch == '=')
                                cout<<"(\t"<<"relop"<<"\t,\t"<<"EQ"<<"\t)\n";//是"=="
                                //return (relop, EQ);
                        else
                        {
                                retract();
                                cout<<"(\t"<<"="<<"\t,\t"<<" "<<"\t)\n";//是"="
                                //return ('=', null);
                        }
                        break;
                case ';':
                        cout<<"(\t"<<";"<<"\t,\t"<<" "<<"\t)\n";//是";"
                        //return (';', null);
                        break;
                default:error();
        }
}
/**
 * 主函数
 */
int main()
{
        while(ptr_input<strlen(input))
                LexicalAnalysis();//进行词法分析
        return 0;
}

4、测试

  如下图图4所示为对程序语句

Hello World;
I am XXX;
x=5;y=9;
if x<y
	x=y;
else
   x=x;
 case 7
  x>7;

  进行词法分析得到的单词二元组。为便于观察,还将单词及其标识符表或常数表中的位置在二元组之后输出,便于观察结果是否正确。
在这里插入图片描述

图4-词法分析得到的单词二元组

5、遇到的问题

(1) 在建立标识符表和常数表时,一开始通过链表的方法来建立,如下建立了对应的标识符表和常数表:

struct NodeId
{
        int index_id;
	char node_id[255];
        NodeId *next;
};
struct NodeNum
{
	int index_num;
	char node_num[255];
        NodeNum *next;
};

但在运行过程中,由于链表是动态分配地址的,所以地址的变化导致每次往表中插入新的字符时都会改变地址,无法得到有效的表。
因此最终选择了使用初始化数组作为标识符和常数存储的位置则解决掉了这个问题。
(2) 再将标识符或常数登记到对应的表中时,忽略了token是有结束符号的,在匹配时一直没有成功。所以在每次匹配时都对得到的单词token处理,去掉结尾的结束符“0”,然后再进行匹配就可以了。

参考文献:《编译原理教程 (第4版)》 胡元义 2016

一、实验目的: 通过设计编制调试一个具体的词法分析程序,加深对词法分析原理的理解。并掌握在对程序设计语言源程序进行扫描过程中将其分解为各类单词的词法分析方法。 编制一个读单词过程,从输入的源程序中,识别出各个具有独立意义的单词,即基本保留字、标识符、常数、运算符、分隔符五大类。并依次输出各个单词的内部编码及单词符号自身值。(遇到错误时可显示“Error”,然后跳过错误部分继续显示) 二、实验预习提示 1、词法分析器的功能和输出格式 词法分析器的功能是输入源程序,输出单词符号。词法分析器的单词符号常常表示成以下的二元式(单词种别码,单词符号的属性值)。本实验中,采用的是按类来安排种别码的方式。 2、部分单词的BNF表示(可参考教材43页的状态转换图) -> ->|| |ε -> -> |ε -> + -> - -> > -> >= 3、 做词法分析器需要把对象语言的词法全部描述出来,在这我们取C语言子集,它的词法如下: (1)关键字 main if else int return void while…….. 所有的关键字都是小写。 (2)专用符号 = + - * / <= > >= == != ; : , { } [ ] ( ) (3)空格和空白、制表符和换行符。 空格一般用来分隔ID、NUM、专用符号和关键字,在词法分析阶段通常被忽略。 各种单词符号的种别码,这是一种符号一个编码的设计。只供参考! 单词符号 种别码 单词符号 种别码 main 2 [ 28 int 1 ] 29 char 3 { 30 If 4 } 31 else 5 , 32 for 6 : 33 while 7 ; 34 ID 10 > 35 NUM 20 = 37 + 22 +”,当前字符为’>’,此时,分析器倒底是将其分析为大于关系运算符还是大于等于关系运算符呢?显然,只有知道下一个字符是什么才能下结论。于是分析器读入下一个字符’+’,这时可知应将’>’解释为大于运算符。但此时,超前读了一个字符’+’,所以要回退一个字符,词法分析器才能正常运行。在分析标识符,无符号整数等时也有类似情况。 5、模块结构 见附图 三、实验过程和指导: (一)准备: 1.阅读课本有关章节,明确语言的语法,写出基本保留字、标识符、常数、运算符、分隔符和程序例。 2.编制好程序。 3.准备好多组测试数据。 (二)上机调试: (三)程序要求: 程序输入/输出示例: 如源程序为C语言。输入如下一段: main() { int a,b; a = 10; b = a + 20; } 要求输出如右图。 (2,“main”) (5,“(” ) (5,“ )” ) (5,“{ ” ) (1,“int” ) (2,“a” ) (5,“,” ) (2,“b” ) (5,“;” ) (2,“a” ) (4,“=” ) (3,“10” ) (5,“;” ) (2,“b” ) (4,“=” ) (2,“a” ) (4,“+” ) (3,“20” ) (5,“;” ) (5,“}” ) 说明: 识别保留字:if、int、for、while、do、return、break、continue; 单词种别码为1。 其他的都识别为标识符;单词种别码为2。 常数为无符号整形数;单词种别码为3。 运算符包括:+、-、*、/、=、>、=、<=、!= ;单词种别码为4。 分隔符包括:,、;、{、}、(、); 单词种别码为5。 以上为参考,具体可自行增删。 程序思路(参考): 这里以开始定义的C语言子集的源程序作为词法分析程序的输入数据。在词法分析中,自文件头开始扫描源程序字符,一旦发现符合“单词”定义的源程序字符串时,将它翻译成固定长度的单词内部表示,并查填适当的信息表。 经过词法分析后,源程序字符串(源程序的外部表示)被翻译成具有等长信息的单词串(源程序的内部表示),并产生两个表格:常数表和标识符表,它们分别包含了源程序中的所有常数和所有标识符。 0.定义部分:定义常量、变量、数据结构。 1.初始化:从文件将源程序全部输入到字符缓冲区中。 2.取单词前:去掉多余空白。 3.取单词后:去掉多余空白(可选,看着办)。 4.取单词:利用实验一的成果读出单词的每一个字符,组成单词,分析类型。(关键是如何判断取单词结束?取到的单词是什么类型的单词?) 5.显示结果。 为了设计好程序,注意以下事情: 1.模块设计:将程序分成合理的多个模块(函数),每个模块做具体的同一事情。 2.写出(画出)设计方案:模块关系简图、流程图、全局变量、函数接口等。 3.编程时注意编程风格:空行的使用、注释的使用、缩进的使用等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zoxiii

越打赏越生长

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值