2023NJU-ICS PA1.2表达式求值 思路详解 心得体会

前言

PA1.2的细节非常非常多,导致这几天花了大量的时间去调试bug,4.3晚上终于过了最后一关“如何测试你的代码”(花了两整天时间才调成功)。虽然耗时巨大,但确实学到了不少东西、训练了能力,于是抽几天时间来此记录一下遇到的问题以及解决和一些心得体会

本文仅提供详细的思路,因为时间有限和比较难整理,所以暂时不展示完整源码,谢谢理解

词法分析

词法分析的目的是把用户输入的表达式分解成一个一个的tokens保存起来,用于后续的运算,这个过程看似直接,但其实会遇到很多细节问题,比如连续的好几个数字字符怎么识别成一个整体的数字?连续的空格字符怎么全部忽略?如果直接用C语言编写,将会十分的复杂,所以使用一个强大的工具——正则表达式

将不同类型的正则表达式映射到不同类型的数字上,组成一个二元组集合,用结构体表示,比如:

struct rule {char *regex;int type;
}rules[]={{" +", TK_NOTYPE}, //others...
}

 前面的" +"表示空格这个字符可以重复1或多次,需要注意的是前面的匹配规则要考虑C语言的转义符使用,比如要匹配除号,要写"\\/",第一个\是为了在C语言中转义/,第二个\是在正则表达式本身转义/。

后面的TK_NOTYPE表示空格,可以在结构体的上面用enum对每一个这种表示起一个编号,如:

enum {TK_NOTYPE = 256,//other...
}

想要使用上面的正则表达式匹配,需要重点关注两个C语言库函数:regcomp和regexec

regcomp可以放在init_regex函数中用于对正则表达式相关的规则初始化,然后init_regex可以放在init_sdb函数中,进行初始化

下面说make_token函数的思路:

make_token()函数的工作方式十分直接, 它用position变量来指示当前处理到的位置, 并且按顺序尝试用不同的规则来匹配当前位置的字符串. 当一条规则匹配成功, 并且匹配出的子串正好是position所在位置的时候, 我们就成功地识别出一个token,把识别到的token放入tokens数组中(具体可以查阅其它博客)

效果举例:

输入:1    +   666* (            6+5)

识别为:

tokens[0].type==NUM,tokens[0].str=="1"

tokens[1].type==TK_NOTYPE

tokens[2].type==ADD

tokens[3].type==TK_NOTYPE

tokens[4].type==NUM,tokens[4].str=="666"

tokens[5].type=='*'

tokens[6].type=='('

tokens[7].type==TK_NOTYPE

tokens[8].type==NUM,tokens[8].str=='6'

tokens[9].type==ADD

tokens[10].type==NUM,tokens[10].str=='5'

tokens[11].type==')'

学会调试

通过PA1.2的学习,我的调试能力有了一定提高,正如讲义所说,调试常见三法宝是:printf、assert、GDB

printf

在某些地方写printf,输入此时某些变量的值进行分析,这也是算法题中常用的技巧

assert

在写下面的eval函数时,assert让我高兴又痛苦,高兴的是每次编译的时候都能找到出错的地方,最常见的两个是:1.括号匹配函数没写好导致的报错 2.除数为0导致的报错(因为这个调bug调了一整天),所以说在合适的位置插入assert(0)可以帮助提高代码的健壮性

以防止除数为0为例,可以写:

 case '/':if(val2 == 0){printf("  val1 = %d p = %d q = %d\n",val1,p,q);assert(0);return 0;}return val1 / val2;

GDB调试器

如讲义所说,要掌握gdb常见命令,如next、step、until、info、file、run、list...写PA期间大量使用了GDB,帮了大忙

递归求值

括号检测函数

我觉得这个函数的目的以下几个:

1.判断一个表达式的括号是否合法,比如(1+3))或(1+((23-6))))就不合法,针对这种不合法的情况,应该直接终止计算

2.去掉两侧多余的函数,比如((1+1)+(2+3)),但(1+1)+(2+3)这种两边的括号当然是不能去掉的

综上,设置三种返回值类型:

return -1:表达式格式有误、终止计算

return 0 :表达式格式正确,且不需要进行去括号处理

return 1 :表达式格式正确,需要去掉两边的括号,进入eval(p+1,q-1)

这里有个问题,多余的括号非去不可吗?是的,在后续“主运算符的寻找”中,如果多这种多余的括号,将会有bug(至少是我的思路会出bug)

继续说这个函数,我们可以分为三个部分:

1.检测

遍历表达式判断格式是否正确,若true,则继续进行下面操作,否则,return -1

2.初步筛选

判断表达式第一个token是否为'('且最后一个token是否为')',若不是,那么一定不用去括号啦,直接return 0

3.递归筛选

直接进入check_parentheses(p+1,q-1),根据返回值分类讨论:

1.若返回-1,说明,(p,q)不能去括号,return 0

2.若返回0或者1,要去括号,return 1

综上,此函数源码为:

static int check_parentheses(int p,int q){//return -1:stop calculate//return 0 :no need to remove parentheses//return 1 :need to remove parentheses
//part1int cnt = 0;for(int i = p; i <= q;i ++){if(tokens[i].type == '(')cnt ++;else if(tokens[i].type == ')')cnt --;if(cnt < 0)return -1;}if(cnt != 0) return -1;
//part2if(tokens[p].type != '(' || tokens[q].type != ')')return 0;
//part3int ret = check_parentheses(p+1,q-1);if(ret == -1)return 0;else if(ret == 0||ret == 1)return 1;return 2;//algorithm_error,提高算法健壮性,防止上面考虑不全而出错
}

主运算符的寻找

解决了括号匹配问题,再看这个核心问题,找主运算符,实际上,满足下面条件即可:

1.一定不在括号里面

2.优先级很小

3.如果几个运算符优先级相同,那么主运算符是最后面那个

所以,整体思路为:

step1:跳过括号

比如(1+1)+(2+3),在遍历token时,遇见第一个'('直接跳到')',但如果没有上面去多余括号的一步,就有:((1+1)+(1+1)),遇见(直接会跳到最右边的')',那么都遍历结束了,如何找主运算符?

step2:遍历token,根据优先级锁定主运算符

这个不多说了,直接上源码、请自行理解 (注意,越往下的优先级越高,可以照着C语言优先级等级表写这个flag大小顺序!)

 int op = -1;int flag = -1 ;for(int i = p; i <= q;i ++){if(tokens[i].type == '('){int cnt = 1;while(cnt != 0){i ++;if(tokens[i].type == '(')cnt ++;else if(tokens[i].type == ')')cnt--;}}if(flag<=7 && tokens[i].type == OR){flag = 7;op = max(op,i);}if(flag<=6 && tokens[i].type == AND){flag = 6;op = max(op,i);}if(flag<=5 && tokens[i].type == NOTEQ){flag = 5;op = max(op,i);}if(flag<=4 && tokens[i].type == EQ){flag = 4;op = max(op,i);}if(flag<=3 && tokens[i].type == LEQ){flag = 3;op = max(op,i);}if(flag<=2 && (tokens[i].type == '+' || tokens[i].type == '-')){flag = 2;op = max(op,i);}if(flag<=1 && (tokens[i].type == '*' || tokens[i].type == '/') ){flag =1;op = max(op,i);}}int op_type = tokens[op].type;

负号和减号的区分

对于更复杂的表达式,比如1 + -2 如何知道后面的"-"是一个负号而不是减号呢?

可以在正式计算的函数(word_t expr(char *e, bool *success))中,在调用eval函数之前,做一些预处理,就以负号判断为例,可以写:

for(int i = 0;i < tokens_len; i ++){if((tokens[i].type == '-' && i == 0)||(tokens[i].type == '-' && i > 0 &&tokens[i-1].type != NUM && tokens[i+1].type == NUM &&tokens[i-1].type != ')')){tokens[i].type = TK_NOTYPE;for(int j = 31;j >= 0;j --){tokens[i+1].str[j] = tokens[i+1].str[j-1];}tokens[i+1].str[0] = '-';for(int j = 0;j < tokens_len; j ++){ if(tokens[j].type == TK_NOTYPE){for(int k = j+1; k < tokens_len;k ++){tokens[k - 1] = tokens[k];}tokens_len --;}}}}

即如果这个'-'在开头或者不在开头但是前面的一个token不是数字、右括号、右边的token是数字,就可以让这个'-'先和后面的数字直接结合,然后调整tokens的位置('-'与后面的数字结合之后就会变成空格)

如何测试你的代码

生成测试用例

这一部分将采用“随机测试”的方法,生成大量的随机表达式,然后放入上面自己写的expr函数中进行测试,以前感觉OJ题目的评判很神奇,能够直接知道自己的哪个数据过不了,做完这个之后,感觉可能和这个测试代码的思路有点相似吧。

生成随机表达式的框架代码如下

static void gen_rand_expr(){
//  buf[0] = '\0' ;if(index_buf > 655){
//    printf("overSize\n");index_buf = 0;}switch(choose(3)){case 0: gen_num();break;case 1:gen('(');gen_rand_expr();gen(')');break;default:gen_rand_expr();gen_rand_op();gen_rand_expr();break;}
}

使用测试用例计算调试器运算准确率

补充gen、choose等函数的代码之后,还需要考虑一些问题,如果生成的表达式格式不对怎么办?如果里面有除数为0的情况又怎么办,我的方法是把报错信息重定向到一个文件中,如果文件为空,则没问题,或者直接根据system函数的返回值确定,如果有问题,则重新生成一次表达式,main源码如下:

int main(int argc, char *argv[]) {int seed = time(0);srand(seed);int loop = 1;if (argc > 1) {sscanf(argv[1], "%d", &loop);}int i;for (i = 0; i < loop; i ++) {index_buf = 0;gen_rand_expr();buf[index_buf] = '\0';long size = 0;sprintf(code_buf, code_format, buf);FILE *fp = fopen("/tmp/.code.c", "w");assert(fp != NULL);fputs(code_buf, fp);fclose(fp);FILE *fp_err = fopen("/tmp/.err_message","w");assert(fp_err != NULL);int ret = system("gcc /tmp/.code.c -o /tmp/.expr 2>/tmp/.err_message");fseek(fp_err, 0, SEEK_END);// 获取文件大小size = ftell(fp_err);fclose(fp_err);if (ret != 0 ||size != 0){index_buf = 0; i--; continue;}fp = popen("/tmp/.expr", "r");assert(fp != NULL);uint32_t result;ret = fscanf(fp, "%u", &result);pclose(fp);printf("%u %s\n", result, buf);index_buf = 0;}return 0;
}

然后在命令行用gcc编译、运行gen-expr.c把输出重定向到input中,用于后序测试调试器运算的准确率

gcc -c -g gen-expr.c 
gcc gen-expr.o -o gen-expr
./gen-expr 100 > input

在sdb.c中增加一条命令test,用于测试准确率

static int cmd_test(char *args){int right_ans = 0;FILE *input_file = fopen("/home/dmz/ics2023/nemu/tools/gen-expr/input", "r");if (input_file == NULL) {perror("Error opening input file");return 1;}char record[1024];unsigned real_val;char buf[1024];// 循环读取每一条记录for (int i = 0; i < 100; i++) {// 读取一行记录if (fgets(record, sizeof(record), input_file) == NULL) {perror("Error reading input file");break;}// 分割记录,获取数字和表达式char *token = strtok(record, " ");if (token == NULL) {printf("Invalid record format\n");continue;}real_val = atoi(token); // 将数字部分转换为整数// 处理表达式部分,可能跨越多行strcpy(buf, ""); // 清空bufwhile ((token = strtok(NULL, "\n")) != NULL) {strcat(buf, token);strcat(buf, " "); // 拼接换行后的部分,注意添加空格以分隔多行内容}// 输出结果printf("Real Value: %u, Expression: %s\n", real_val, buf);bool flag = false;unsigned res = expr(buf,&flag);if(res == real_val)right_ans ++;}printf("test 100 expressions,the accuracy is %d/100\n",right_ans);fclose(input_file);return 0;
}

然后进入nemu,输入test,发现很多表达式依旧发生报错(除数为0的现象),而且是我写的sdb里面发生了除数为0,但是当我把这个表达式放入devC++直接运算,并没有报错现象,经过一番探索,我尝试了如下代码:

#include <stdio.h>
int main() { unsigned result = 1000/(-120);printf("%u", result); return 0; 
}

结果是一个很大的数,但用我自己写的sdb的p命令来算,算出的是0,最终知道了:对于一个表达式在devC++计算的过程中,先是当成有符号数处理了,比如上面的算出来是-8,然后再转换成了无符号数,所以很大,但我写的eval的返回类型就是uint32_t,就会先把-120变成很大的数,然后相除后得0!

当把此返回类型改成int,仅在最后的expr函数中转成uint32_t之后:

进入nemu、输入test,成功!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/805735.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

echarts地图自定义label属性以及引入china.js

效果图: 要点1:calc函数 重点&#xff1a;在于mapChart的height可以写成函数以便适配不同尺寸&#xff1b; <div class"content-map"><div class"wai-top-box" style"width: 100%; height: 100%"><div id"mapChart" s…

Host Aware SMR

SMR 简介 首先给一点前置SMR知识。 SMR优势&#xff1a;Capacity的提升。 看图&#xff1a;由于重叠Track使得存储密度得到了提升。但是由于Track的重叠&#xff0c;使得SMR只能顺序写。 在SMR中&#xff0c;多个Track组成一个Band&#xff0c;各个Band之间可以随机写 这个 …

94岁诺奖得主希格斯去世,曾预言「上帝粒子」的存在

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 一位用诗意的语言揭示宇宙秘密的人。 一位 94 岁伟大科学家的逝世&#xff0c;引发了人们广泛…

RUST语言值所有权之内存复制与移动

1.RUST中每个值都有一个所有者,每次只能有一个所有者 String::from函数会为字符串hello分配一块内存 内存示例如下: 在内存分配前调用s1正常输出 在分配s1给s2后调用报错 因为s1分配给s2后,s1的指向自动失效 s1被move到s2 s1自动释放 字符串克隆使用 所有整数类型,布尔类型 …

nandgame中的Code generation(代码生成)

题目说明&#xff1a; 代码生成为语言的语法规则定义代码生成&#xff0c;以支持加法和减法。 您可以使用在前面级别中定义的堆栈操作&#xff08;如ADD和SUB&#xff09;。代码生成模板通常需要包含规则中其他符号的代码。 这些可以通过方括号中的符号名称插入。例如&#xf…

day03-java类型转换和运算符

3.1 表达式和语句 表达式一共分为三种&#xff1a; &#xff08;1&#xff09;变量或常量 运算符构成的计算表达式 &#xff08;2&#xff09;new 表达式&#xff0c;结果是一个数组或类的对象。&#xff08;后面讲&#xff09; &#xff08;3&#xff09;方法调用表达式&…

接口自动化进阶: Pytest之Fixture拓展及conftest.py加载机制!

Pytest是一个功能强大的Python测试框架&#xff0c;它提供了很多有用的功能和扩展机制。其中之一是Fixture&#xff0c;Fixture是pytest中的一个装饰器&#xff0c;可以用来提供测试所需的数据和对象。 本篇文章将从头开始&#xff0c;详细介绍如何使用Fixture进行接口自动化测…

Linux安装Oracle11g(无图形界面下的静默安装)

Oracle11g安装文档-Linux静默安装 环境准备安装数据库配置监听器创建数据库测试打开防火墙 环境准备 创建组和用户 [rootlocalhost ~]# groupadd oinstall #创建oinstall组 [rootlocalhost ~]# groupadd dba  #创建dba组 [rootlocalhost ~]# useradd -g oinstall -G dba -m…

鸿蒙HarmonyOS开发实例:【分布式关系型数据库】

介绍 本示例使用[ohos.data.relationalStore]接口和[ohos.distributedDeviceManager] 接口展示了在eTS中分布式关系型数据库的使用&#xff0c;在增、删、改、查的基本操作外&#xff0c;还包括分布式数据库的数据同步同能。 效果预览 使用说明: 启动应用后点击“ ”按钮可…

《QT实用小工具·十七》密钥生成工具

1、概述 源码放在文章末尾 该项目主要用于生成密钥&#xff0c;下面是demo演示&#xff1a; 项目部分代码如下&#xff1a; #pragma execution_character_set("utf-8")#include "frmmain.h" #include "ui_frmmain.h" #include "qmessag…

ArrayList中多线程的不安全问题

ArrayList中的不安全问题 正常的输出 List<String> list Arrays.asList("1","2","3"); list.forEach(System.out::println);为什么可以这样输出&#xff0c;是一种函数是接口&#xff0c;我们先过个耳熟 Arrys.asList是返回一个ArrayL…

进程间通信 (匿名管道)

一、进程间通信的概念 进程间通信是一个进程把自己的数据交给另一个进程&#xff0c;它可以帮助我们进行数据传输、资源共享、通知事件和进程控制。 进程间通信的本质是让不同的进程看到同一份资源。因此&#xff0c;我们要有&#xff1a; 1、交换数据的空间。2、这个空间不能由…

hadoop103: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).

分析&#xff1a; 在启动hadoop服务的时候&#xff0c;遇到了这个问题&#xff1a; hadoop103: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password). 这个一看就是&#xff0c;密钥问题 于是ssh 主机名就行测试 需要输入密码&#xff0c;就说明这里有问…

C++笔记(函数重载)

目录 引入&#xff1a; 定义&#xff1a; 易错案例&#xff1a; 引入&#xff1a; 对于实现相似功能的函数&#xff0c;在命名时&#xff0c;我们常会出现命名重复的问题。对于C语言&#xff0c;编译器遇到这种命名重复的情况&#xff0c;会进行报错。而我们的C为了更方便程…

【计算机毕业设计】校园网书店系统——后附源码

&#x1f389;**欢迎来到我的技术世界&#xff01;**&#x1f389; &#x1f4d8; 博主小档案&#xff1a; 一名来自世界500强的资深程序媛&#xff0c;毕业于国内知名985高校。 &#x1f527; 技术专长&#xff1a; 在深度学习任务中展现出卓越的能力&#xff0c;包括但不限于…

分布式锁-redission

5、分布式锁-redission 5.1 分布式锁-redission功能介绍 基于setnx实现的分布式锁存在下面的问题&#xff1a; 重入问题&#xff1a;重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中&#xff0c;可重入锁的意义在于防止死锁&#xff0c;比如HashTable这样的代码…

pycharm一直打不开

一直处在下面的页面&#xff0c;没有反应 第一种方案&#xff1a; 以管理员身份运行 cmd.exe&#xff1b;在打开的cmd窗口中&#xff0c;输入 netsh winsock reset &#xff0c;按回车键&#xff1b;重启电脑&#xff1b;重启后&#xff0c;双击pycharm图标就能打开了&#xf…

深度理解运放增益带宽积

原文来自微信公众号&#xff1a;工程师看海&#xff0c;与我联系&#xff1a;chunhou0820 看海原创视频教程&#xff1a;《运放秘籍》 大家好&#xff0c;我是工程师看海。 增益带宽积是运算放大器的重要参数之一&#xff0c;指的是运放的增益和带宽的乘积&#xff0c;这个乘积…

STC89C52学习笔记(四)

STC89C52学习笔记&#xff08;四&#xff09; 综述&#xff1a;本文讲述了在STC89C51中数码管、模块化编程、LCD1602的使用。 一、数码管 1.数码管显示原理 位选&#xff1a;对74HC138芯片的输入端的配置&#xff08;P22、P23、P24&#xff09;&#xff0c;来选择实现位选&…

玩转ChatGPT:Kimi测评(图片识别)

一、写在前面 ChatGPT作为一款领先的语言模型&#xff0c;其强大的语言理解和生成能力&#xff0c;让无数用户惊叹不已。然而&#xff0c;使用的高门槛往往让国内普通用户望而却步。 最近&#xff0c;一款由月之暗面科技有限公司开发的智能助手——Kimi&#xff0c;很火爆哦。…