【编译原理】计算器实现(C语言)
这几天编译原理课留了个编写一个简单的计算器,要求能够检查错误并进行计算。其实也就是完成词法分析,语法分析,再完成一个后缀表达式的计算就好了。
这里找到了一个比较简单的方法,这里记录一下。
题目要求
简单分析
整个程序其实整体上大致分成四部分去做
1.读文件存储内容(这里的文本是以字符的形式存储的)
2.词法分析,把字符拆分成一个个的token
3.语法分析,对拆分的token进行分析,确定有无错误
4.后缀表达式求值,在确认格式无误后进行后缀表达式求值并输出
(1)读文件
readFile()
函数完成了读入字符串到str数组中保存起来,方便后续操作,在这里处理了一个结尾是否为“ . “的错误,比较简单没什么好说的。
下面是具体函数 readFile
。
unsigned long readFile(char *filePath, char str[]) {
FILE *fp = fopen(filePath, "r");//打开文件
if (fp == NULL) {
printf("打开文件出错,请确认文件存在当前目录下!\n");
exit(0);
}
unsigned long i;
i = fread(str, 1, MaxLen, fp);
if (i >= MaxLen) {
printf("文件过大!请重新输入一个更小的文件。\n");
exit(1);
}
if (str[i - 1] != '.') {
printf("Error, Please end up with \".\"\n");
is_error = 1;
}
str[i] = '\0';
fclose(fp);
return i;
}
(2)词法分析
词法分析函数Scaner()
这里的词法分析生成token,如果了解过一些编译原理词法分析的内容的话,这里的这个方法其实是有很多简化的,他其实只处理并储存了数字,标识符(变量名、int 、float) 对于其它的标点,运算符等符号其实都没有进行处理,只是将其分割但是没有保存。
这里用了一个链表把标识符保存了起来,方便后续的检测
值得一提的是为什么这里用了while循环,这个循环,巧妙地解决的空格的问题,也就是如果有多余的空格,这里会直接跳过,很巧妙。
这是 Scaner()
void Scaner() {
int i, is_float = -1;
while (token = *src) {
src++;
//词法分析开始
if (token == '.')
return;
else if (token == '\n')
line++;
else if (token == '+' || token == '-' || token == '*' || token == '/'
|| token == '=' || token == ';' || token == '(' || token == ')')
return;
//处理标识符
else if ((token >= 'a' && token <= 'z') || (token >= 'A' && token <= 'Z')) {
new_id = 1;
char *front = src - 1;
while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9'))
src++;
char name1[MaxLen];
int n = src - front;
for (i = 0; i < n; i++)
name1[i] = *front++;
name1[i] = '\0';
//判断标识符是否重复
for (current = head->next; current != NULL; current = current->next) {
if (!Strcmp(current->name, name1)) {
current1 = current;
token = current->stamp;
new_id = 0;
return;
}
}
token = Id;
if (current1->type_float == 1)
is_float = 1;
ListPush(token, name1, -1, 0, is_float);
return;
}
//处理整数,浮点数
else if (token >= '0' && token <= '9') {
int float_not = 1;
int num = 1;
token_value = (float)token - '0';
while (*src >= '0' && *src <= '9' || *src == '.') {
if (*src == '.') {
float_not = 0;
src++;
continue;
}
if (float_not) {
token_value = token_value * 10 + (*src - '0');
src++;
}
else {
num = num * 10;
token_value += (float)((*src - '0') * 1.0 / num);
src++;
}
}
token = Num;
return;
}
else if (token == ' ')
continue;
else
return;
}
}
后面还用match()
把Scaner()
进行了包装,这里的match
在语法分析里有妙用。
(3)语法分析
Reader()
函数做了语法分析。
这里先介绍match()
的作用,就是判断给定的格式是不是当前token指向的字符格式,如果是则扫描下一个token,这里的token是指一个字符,一个数字,一个标识符。也就是说match()
做的就是语法里面的匹配。
这样来再看语法分析,根据当年token的类型,决定进行什么语句的分析。
例如说开头的是变量Id,就是赋值语句,依次去match(Id)、match(=)、match(;)。这里为什么没有去求值,后面的后缀表达式介绍。
开头的是int 就要匹配定义语句:match(int)、match(=)、match(Id)、match(;)。
基本上搞清楚token指向的是什么,就很容易理解了。
Reader()
void Reader() {
if (token == Id) {
if (new_id) {
ListPop();
printf("Line %d: Error: Undeclared Variable!\n", line);
is_error = 1;
}
Token *assign = current;
match(Id);
match('=');
if (current->type_float == 1) {
is_typeint = 1;
}
float value;
value = expr();
if (((value - (int)value) == 0) && is_typeint == 1) {
printf("Line %d: Error: Illegal Conversion!\n", line);
is_error = 1;
}
if (assign->type == Float)
assign->value = value;
else
assign->value = (int)value;
match(';');
}
else if (token == Int) {
new_id = 1;
match(Int);
if (!new_id) {
printf("Line %d: Warning: The Variable Is Repeatedly Declared!\n", line);
is_error = 1;
}
current->type = Int;
match(Id);
match(';');
}else if (token == Float) {
new_id = 1;
match(Float);
if (!new_id) {
printf("Line %d: Warning: The Variable Is Repeatedly Declared!\n", line);
is_error = 1;
}
current->type = Float;
match(Id);
match(';');
}
else if (token == Write) {
float output;
int output_type;
match(Write);
match('(');
if (new_id) {
ListPop();
printf("Line %d: Error:Undeclared Variable!\n", line);
is_error = 1;
}
else {
output = current->value;
output_type = current->type;
}
match(Id);
if (!is_var)
{
match(Id);
}
match(')');
match(';');
if (!is_error) {
if (output_type == Int)
printf(" %d \n", (int)output);
else if (output_type == Float)
printf(" %f \n", output);
}
}
else if (token == '.')
return;
}
(4)后缀表达式求值
后缀表达式求值本质上就是一个递归(或者堆栈)区分出运算符号优先级的过程,具体操作可以参照数据结构课本的堆栈相关章节。
这里只说这里的具体实现,因为我们在Scaner()
函数重拆分的时候会得到具体的数字和运算符,而我们的Reader()
又是一个个token进行匹配的,所以当我们进行计算时,可以先行计算出来值。
总结
基本内容就结束了,详细的源码以及说明放在这里了,如何运行请参照里面的。readme.md
链接: Caculator.