【编译原理】实验一:实现简易词法分析器

C语言实现

C语言子集的单词符号及内码值

所设计的输出单词符号二元式的组成为:

(种别编码,单词自身的值)

例如(1,while)、(7,123)、(6,hello)。
宏定义助记符id、num、relop。

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

实验环境

windows 10
visual studio 2019

测试文件test.txt

if atest=1
	hello world!;
else
	d=d*atest-1;
while btest<=3
	atest+c=1213;
switch syx
	case 1	atest<btest;
	case 2	atest==btest;

函数说明

代码主要基于《编译原理教程(第四版)》1 第二章 词法分析(p14)实现,尽量去贴合书上所给例题。

0 变量说明

character		字符变量,存放最新读入的源程序字符
token			字符数组,存放构成单词符号的字符串

1 对输入串进行预处理

主要对输入串进行剔除多余的空白符操作。调用getbe()函数,若character中的字符为空白,则调用getchar_syx()函数(书上为getchar()),直至character为非空白符为止。

/*****************************************************************************
 *函数名称:getbe()
 *函数类型:void
 *参数:void
 *功能:滤白。若character中的字符为空白,则调用getchar_syx(),直至character为非空白字符为止
*****************************************************************************/
void getbe() {
	while (character == ' ')
		getchar_syx();
}

其中,getchar_syx()用来逐个读取文件中的字符。

/*****************************************************************************
 *函数名称:getchar_syx()
 *函数类型:void
 *参数:void
 *功能:逐个读取文件中的字符
 *****************************************************************************/
void getchar_syx() {
	if (fin)
		character = fgetc(fin);
}

2 将读入的字符链接成字符串保存在token中

调用concatenation()函数,将token中的字符串与character中的字符连接并作为token中新的字符串。

/*****************************************************************************
 *函数名称:concatenation()
 *函数类型:void
 *参数:void
 *功能:将token中的字符串与character中的字符连接并作为token中新的字符串
 *****************************************************************************/
void concatenation() {
	token[token_point] = character;
	token_point++;
	token[token_point] = '\0';//字符串结束标志
}

3 判断character中的字符是否为字母或数字

调用letter()函数和digit()函数,判断character中的字符是否为字母和数字的布尔函数,是则返回true(即1),否则返回false(即0)。

/*****************************************************************************
 *函数名称:letter()
 *函数类型:_Bool
 *参数:void
 *功能:判断character中的字符是否为字母,是则返回true,否则返回false
 *****************************************************************************/
_Bool letter() {
	if ((character >= 'a') && (character <= 'z') || (character >= 'A') && (character <= 'Z'))
		return 1;
	else
		return 0;
}

/*****************************************************************************
 *函数名称:digit()
 *函数类型:_Bool
 *参数:void
 *功能:判断character中的字符是否为数字,是则返回true,否则返回false
 *****************************************************************************/
_Bool digit() {
	if ((character >= '0') && (character <= '9'))
		return 1;
	else
		return 0;
}

4 判断token中的字符串是否为保留字

调用reserve()函数,按token数组中的字符串查保留字表,若是保留字的话,返回它的编码值,否则返回0值。
这里是定义了一个保留字表,存储保留字名称,用数组下标标识其编码值。

/*****************************************************************************
 *函数名称:reserve()
 *函数类型:int
 *参数:void
 *功能:判别token数组中的字符串是否为保留字,若为保留字则返回它的编码,否则返回0值
 *****************************************************************************/
int reserve() {
	for (int i = 1; i < reserved_Len; i++) {
		if (strcmp(token, reserved_Word[i]) == 0) {
			return i;
		}
	}
	return 0;
}

5 指针回退

调用retract()函数,扫描指针回退一个字符,同时将character置为空白。

/*****************************************************************************
 *函数名称:retract()
 *函数类型:void
 *参数:void
 *功能:扫描指针回退一个字符,同时将character置为空白
 *****************************************************************************/
void retract() {
	fseek(fin, -1, SEEK_CUR);
	character = ' ';
}

6 标识符\常数登记入表

调用buildlist()函数,将标识符登记到符号表中或将常数登记到常数表中。
(1)如果是常数,需要将字符串转换成数字(利用atoi函数)。
(2)无论是标识符还是常数,在登记入表时都需要查表看看表中是否已有该项了,如果表中没有项,才添加进表。

/*****************************************************************************
 *函数名称:buildlist()
 *函数类型:void
 *参数:void
 *功能:将标识符登记到符号表中或将常数登记到常数表中
 *****************************************************************************/
void buildlist() {
	char temp = token[0] - '0';//用于判断当前token中的字符串是标识符还是常数
	/************************将常数登记到常数表中*****************************/
	int temp_num = 0;//存放转换后的常数值
	if (temp >= 0 && temp <= 9) {//如果是数字
		temp_num = atoi(token);//利用atoi函数将token中的字符串转换成常数

		int i;
		for (i = 0; i < number_point; i++) {
			if (temp_num == number_table[i]) {//如果常数表中已存在该常数
				present_point = i;
				break;
			}
		}
		if (i == number_point) {//如果常数表中没有该常数,则登记该常数
			number_table[number_point] = temp_num;
			present_point = number_point;
			number_point++;
		}
	}
	/************************将标识符登记到常数表中*****************************/
	else {
		int i = 0;
		for (i; i < id_point; i++) {
			if (strcmp(token, identifier_table[i]) == 0) {//如果标识符表中已存在该字符串
				present_point = i;
				break;
			}
		}
		if (i == id_point) {//如果标识符表中没有该字符串,则登记该标识符
			strcpy_s(identifier_table[i],sizeof(token),token);
			present_point = id_point;
			id_point++;
		}
	}
}

7 词法分析

唔,尽量贴合伪代码去写的。只是实验一,虽然后来课设也用到了,比较菜就看看吧。

/*****************************************************************************
 *函数名称:test()
 *函数类型:void
 *参数:void
 *功能:词法分析
 *****************************************************************************/
void test() {
	//strcpy_s(token, sizeof(token), " ");	//对token数组初始化
	token_point = 0;						//token指针初始化
	getchar_syx();							//获取字符
	getbe();  								//滤除空格
	switch (character) {
	case 'a':
	case 'b':
	case 'c':
	case 'd':
	case 'e':
	case 'f':
	case 'g':
	case 'h':
	case 'i':
	case 'j':
	case 'k':
	case 'l':
	case 'm':
	case 'n':
	case 'o':
	case 'p':
	case 'q':
	case 'r':
	case 's':
	case 't':
	case 'u':
	case 'v':
	case 'w':
	case 'x':
	case 'y':
	case 'z':
		/*字母开头,为标识符/保留字*/
		while (letter() || digit()) {
			/*标识符包括字母和数字*/
			concatenation();    //将当前读入的字符送入token数组
			getchar_syx();
		}
		retract();            	//扫描指针回退一个字符
		int c = reserve();
		if (c == 0) {
			buildlist();        //将标识符登录到符号表中
			/*return(id, 指向id的符号表入口指针);*/
			fprintf(fout, "(%d,%s)\t\t指向id的符号表入口指针:\t%d\n", id, identifier_table[present_point], present_point);
		}
		else {
			/*return(保留字码,null);*/
			fprintf(fout, "(%d,%s)\n", c, reserved_Word[c]);
		}
		break;
	case '0':
	case '1':
	case '2':
	case '3':
	case '4':
	case '5':
	case '6':
	case '7':
	case '8':
	case '9':
		/*数字开头,为常数*/
		while (digit()) {
			/*常数只包括数字*/
			concatenation();
			getchar_syx();
		}
		retract();
		buildlist();          //将常数登录到常数表中
		/*return(num, num的常数表入口指针);*/
		fprintf(fout, "(%d,%d)\t\t指向num的符号表入口指针:\t%d\n", num, number_table[present_point], present_point);
		break;
	case '+':
		/*return('+', null);*/
		fprintf(fout, "(%c,_)\n", '+');
		break;
	case '-':
		/*return('-', null);*/
		fprintf(fout, "(%c,_)\n", '-');
		break;
	case '*':
		/*return('*', null);*/
		fprintf(fout, "(%c,_)\n", '*');
		break;
	case '<':
		getchar_syx();
		if (character == '=')
			fprintf(fout, "(%d,%s)\n", relop, "LE");
		/*reutrn(relop, LE);*/
		else {
			retract();
			/*return(relop, LT);*/
			fprintf(fout, "(%d,%s)\n", relop, "LT");
		}
		break;
	case '=':
		getchar_syx();
		if (character == '=')
			fprintf(fout, "(%d,%s)\n", relop, "EQ");
		/*return(relop, EQ);*/
		else {
			retract();
			fprintf(fout, "(%c,_)\n", '=');
			/*return('=', null);*/
		}
		break;
	case ';':
		fprintf(fout, "(%c,_)\n", ';');
		/*return(';', null);*/
		break;
	default:
		if (character == '\n' || character == '\t' || character == EOF)
			break;
		error();			//调用出错函数
	}
}

8 出错处理

调用error()函数,出现非法字符,显示出错信息。

/*****************************************************************************
 *函数名称:error()
 *函数类型:void
 *参数:void
 *功能:出现非法字符,显示出错信息
 *****************************************************************************/
void error() {
	printf("非法字符:%c\n", character);
}

9 主函数

关于errno返回值,看大佬的 Linux errno详解.
不想了解的话,只要知道err_in和err_out的返回值为0正确就行。

int main() {
	errno_t err_in, err_out;
	err_in = fopen_s(&fin, "E:\\test.txt", "r");
	err_out = fopen_s(&fout, "E:\\testout.txt", "w+");
	while (character != EOF)
		test();
	if (fin)
		fclose(fin);
	if (fout)
		fclose(fout);
	return 0;
}

运行结果

保存在testout.txt中。

(2,if)
(6,atest)		指向id的符号表入口指针:	0
(=,_)
(7,1)		指向num的符号表入口指针:	0
(6,hello)		指向id的符号表入口指针:	1
(6,world)		指向id的符号表入口指针:	2
(;,_)
(3,else)
(6,d)		指向id的符号表入口指针:	3
(=,_)
(6,d)		指向id的符号表入口指针:	3
(*,_)
(6,atest)		指向id的符号表入口指针:	0
(-,_)
(7,1)		指向num的符号表入口指针:	0
(;,_)
(1,while)
(6,btest)		指向id的符号表入口指针:	4
(11,LE)
(7,3)		指向num的符号表入口指针:	1
(6,atest)		指向id的符号表入口指针:	0
(+,_)
(6,c)		指向id的符号表入口指针:	5
(=,_)
(7,1213)		指向num的符号表入口指针:	2
(;,_)
(4,switch)
(6,syx)		指向id的符号表入口指针:	6
(5,case)
(7,1)		指向num的符号表入口指针:	0
(6,atest)		指向id的符号表入口指针:	0
(11,LT)
(6,btest)		指向id的符号表入口指针:	4
(;,_)
(5,case)
(7,2)		指向num的符号表入口指针:	3
(6,atest)		指向id的符号表入口指针:	0
(11,EQ)
(6,btest)		指向id的符号表入口指针:	4
(;,_)

lex实现

%{
	#include <stdio.h>
	#include <stdlib.h>
	int count = 0;
	/*
	 * digit		数字
	 * letter		字母
	 * reservedWord	保留字
	 * identifier	标识符
	 * constant		常数
	 * operator		运算符
	 * delim		空白符
	 * whitespace	空白符组合
	 * other		非法字符
	 */
%}

digit			[0-9]
letter			[a-zA-Z]
reservedWord	[w][h][i][l][e]|[i][f]|[e][l][s][e]|[s][w][i][t][c][h]|[c][a][s][e]
identifier		{letter}({letter}|{digit})*
constant		{digit}+
operator		\+|-|\*|<=|<|==|=|;
delim			[ \t\n\r]
whitespace		{delim}+
other			.
%%
{reservedWord}  {count++;fprintf(yyout,"%d\t(1,‘%s’)\n",count,yytext);}
{identifier}    {count++;fprintf(yyout,"%d\t(2,‘%s’)\n",count,yytext);}
{constant}		{count++;fprintf(yyout,"%d\t(3,‘%s’)\n",count,yytext);}
{operator}      {count++;fprintf(yyout,"%d\t(4,‘%s’)\n",count,yytext);}
{whitespace}    { /* do    nothing*/ }
{other}			{fprintf(yyout,"非法字符:'%s'\n",yytext);}

%%
void main()
{
	yyin=fopen("test.txt","r");
	yyout=fopen("testout.txt","w+");
    	yylex(); /* start the analysis*/
	fclose(yyout);
	fclose(yyin);
}
 int yywrap()
 {
 	return 1;
 }

运行结果

testout.txt

1	(1,‘if’)
2	(2,‘atest’)
3	(4,‘=’)
4	(3,‘1’)
5	(2,‘hello’)
6	(2,‘world’)
非法字符:'!'
7	(4,‘;’)
8	(1,‘else’)
9	(2,‘d’)
10	(4,‘=’)
11	(2,‘d’)
12	(4,‘*’)
13	(2,‘atest’)
14	(4,‘-’)
15	(3,‘1’)
16	(4,‘;’)
17	(1,‘while’)
18	(2,‘btest’)
19	(4,‘<=’)
20	(3,‘3’)
21	(2,‘atest’)
22	(4,‘+’)
23	(2,‘c’)
24	(4,‘=’)
25	(3,‘121’)
26	(4,‘;’)
27	(1,‘switch’)
28	(2,‘syx’)
29	(1,‘case’)
30	(3,‘1’)
31	(2,‘atest’)
32	(4,‘<’)
33	(2,‘btest’)
34	(4,‘;’)
35	(1,‘case’)
36	(3,‘2’)
37	(2,‘atest’)
38	(4,‘==’)
39	(2,‘btest’)
40	(4,‘;’)

后记

这个词法分析器在最后课设(实现一个小型编译程序)的时候也用到了
下载地址👉词法分析器.

参考文献


  1. 胡元义.编译原理教程(第四版)[M].西安电子科技大学出版社:西安,2015:9-34. ↩︎

  • 12
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
C语言的一个子集编译器是一种用于将C语言子集的源代码转换为可执行的目标代码的工具。编译过程主要包括四个阶段:词法分析、语法分析、中间代码生成和目标代码生成。 词法分析是编译器的第一步,它将源代码拆分成一个个的词法单元,比如关键字、标识符、运算符、常数等。词法分析器会忽略源代码中的空格和注释,并将每个词法单元提供给语法分析器进行下一步处理。 语法分析是编译器的第二步,它将词法分析器提供的词法单元按照语法规则进行组织,生成一个树状的语法结构,这个树被称为语法分析树(语法树)。语法分析器使用语法规则来验证源代码的语法正确性,并生成相应的语法树。 中间代码生成是编译器的第三步,它将语法分析树转换为一种中间表示形式,通常是一种抽象的汇编语言。中间代码是一种介于源代码和目标代码之间的中间表示形式,它能够更容易地进行分析、优化和生成目标代码。 目标代码生成是编译器的最后一步,它将中间代码转换为目标机器能够运行的机器代码。目标代码生成器将中间代码中的每条指令转换为与目标机器体系结构相对应的机器指令,并生成可执行的目标代码文件。 综上所述,C语言的一个子集编译器通过词法分析将源代码中的字符转换为词法单元,然后使用语法分析将词法单元组织成语法树,接着将语法树转换为中间代码,最后通过目标代码生成将中间代码转换为可执行的目标代码文件。这个编译器的功能是将C语言子集的源代码转换为可执行的目标代码,让计算机能够理解和执行这段代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

社恐患者

赚钱不易呜呜呜

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

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

打赏作者

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

抵扣说明:

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

余额充值