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
//part1
  int 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;
//part2
  if(tokens[p].type != '(' || tokens[q].type != ')')return 0;
//part3
  int 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, ""); // 清空buf
        while ((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,成功!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

07 Php学习:运算符

PHP 算术运算符 在 PHP 中&#xff0c;算术运算符用于执行基本的数学运算&#xff0c;包括加法、减法、乘法、除法、取余数&#xff0c;负数运算、取反和并置运算。以下是这些运算符的详细解释和示例&#xff1a; 加法运算符 &#xff1a;用于将两个数值相加。 $a 5; $b 3;…

MySQL innoDB存储引擎多事务场景下的事务执行情况

一、背景 在日常开发中&#xff0c;对不同事务之间的隔离情况等理解如果不够清晰&#xff0c;很容易导致代码的效果和预期不符。因而在这对一些存在疑问的场景进行模拟。 下面的例子全部基于innoDB存储引擎。 二、场景&#xff1a; 2.1、两个事务修改同一行记录 正常来说&…

基于ssm乐购游戏商城系统论文

摘 要 随着社会的发展&#xff0c;游戏品种越来越多&#xff0c;计算机的优势和普及使得乐购游戏商城系统的开发成为必需。乐购游戏商城系统主要是借助计算机&#xff0c;通过对信息进行管理。减少管理员的工作&#xff0c;同时也方便广大用户对个人所需信息的及时查询以及管理…

IO流【 文件字符输入、出流;带缓冲区的字符输入、出流;对象流】

day36 IO流 字符流继承图 字符流 继day35 应用场景&#xff1a;操作纯文本数据 注意&#xff1a;字符流 字节流编译器 编译器&#xff1a;可以识别中文字符和非中文字符&#xff0c;非中文字符获取1个字节&#xff08;一个字节一个字符&#xff09;&#xff0c;编译器会根据…

Electron打包vue+java+nginx 踩坑记录

记录下遇到的问题&#xff1a; ⚠注意&#xff1a;64位系统和32位系统的配置不太一样 1、运行npm run packager失败 原因&#xff1a;在package.json没有对应命令 解决&#xff1a;在package.json 中添加对应命令&#xff0c;其中testApp是你想要的输入的项目名称&#xff0…

langchain 使用本地通义千问

langchian 使用已经下载到本地的模型&#xff0c;我们使用通义千问 显存&#xff1a;24G 模型&#xff1a;qwen1.5-7B-Chat&#xff0c;qwen-7B-Chat 先使用 qwen-7B-Chat&#xff0c;会报错用不了&#xff1a; 看了下是不支持这中模型&#xff0c;但看列表中有一个 Qwen 字样…

Asterisk语音卡驱动DAHDI 3.2版本对于TDM410P的支持

目录 DAHDITDM410base.c什么是电话语音卡 资本掌控下的Asterisk虽然继续履行开源社区的承诺&#xff0c;但实际上小手还是会四处乱摸&#xff0c;比如对于Asterisk硬件驱动DAHDI&#xff0c;就做了些隐蔽的小动作。 DAHDI DAHDI 全称是 Digium Asterisk Hardware Device Inter…

xss.pwnfunction-Ugandan Knuckles

这个是把<>过滤掉了所以只能用js的事件 ?weya"onfocus"alert(1337)" autofocus"

不允许在constexpr函数中进行声明

这是我用pycharm在windows系统下复现sfm深度学习网络(Deep Two-View Structure-from-Motion Revisited&#xff09;遇见的问题&#xff0c;复现时有段代码pytorch扩展cuda/c&#xff0c;pycharm中出现C标准相关的报错如下&#xff1a; 在网上查找很久无果&#xff0c;后面通过…

java国产化云HIS基层医院系统源码 SaaS模式

目录 ​ 云HIS开发环境 功能模块介绍&#xff1a; 1、门诊模块 2、住院模块 3、药房、药库模块 ​编辑 4、电子病历模块 5、统计报表模块 6、系统管理模块 系统优势 云his之电子病历子系统功能 云 his 系统是运用云计算、大数据、物联网等新兴信息技术&#xff0c;按…

python爬虫———激发学习兴趣的案列(第十三天)

&#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; &#x1f388;&#x1f388;所属专栏&#xff1a;python爬虫学习&#x1f388;&#x1f388; ✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天…

linux内核驱动-在内核代码里添加设备结点

linux中&#xff0c;一切皆文件 我们在用户层用一些系统函数&#xff08;如&#xff1a;fopen等等&#xff09;时&#xff0c;会进入内核&#xff0c;内核会在字符注册了的设备号链表中查找。如果找到就运行我们写的设备文件的&#xff08;驱动&#xff09;函数 我们在前面已经…

[方案实操|数据技术]数据要素十大创新模式(1):基于区块链的多模态数据交易服务平台

“ 区块链以其公开共享、去中心化、不可篡改、可追溯和不可抵赖等优势&#xff0c;吸引了包括金融业、医疗业和政府部门等众多利益相关方的极大兴趣&#xff0c;被认为是解决数据安全交换问题的合适方案。” 武汉东湖大数据科技股份有限公司凭借基于区块链的多模态数据交易服务…

C语言---顺序表(一)

文章目录 前言1.数据结构1.1.什么是数据结构1.2.为什么需要数据结构&#xff1f; 2.顺序表2.1.顺序表的概念2.2.顺序表分类 3.动态顺序表的实现 前言 在前面20多篇博客中&#xff0c;我们已经学习完了C语言知识&#xff0c;下面我们开启新的一章—数据结构。 1.数据结构 1.1.…

消息队列介绍

MQ&#xff08;Message Queue&#xff09;&#xff1a;消息队列。通过典型的生产者和消费者模型&#xff0c;生产者不断向消息队列中生产消息&#xff0c;消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的&#xff0c;而且只关心消息的发送和接收&#xff0c;没有…

106. 跑步锻炼(结果填空)

public class Main { public static void main(String[] args) { int startYear 2000; int startMonth 1; int startDay 1; // 周六 int endYear 2020; int endMonth 10; int endDay 1; // 周四 int totalDistance 0; // 计算开始日期到结束日期之间的每一天 …

分布式文件系统

引言&#xff1a; GFS是一个可扩展的分布式文件系统&#xff0c;用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的普通硬件上&#xff0c;并提供容错功能。它可以给大量的用户提供总体性能较高的服务。 一、 GlusterFS 概述 1.1 GlusterFS简介 GlusterFS 是…

7B超越百亿级,北大开源aiXcoder-7B最强代码大模型,企业部署最佳选择

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 ​ 对代码大模型而言&#xff0c;比能做编程题更重要的&#xff0c;是看是能不能适用于企业…

LMDoply部署实战

使用LMDeoply部署各类开源大模型&#xff0c;进行推理实践。 一. 环境准备 1. 创建Conda环境 studio-conda -t lmdeploy -o pytorch-2.1.2 2. 安装LMDeploy 激活刚刚创建的虚拟环境。 conda activate lmdeploy 安装0.3.0版本的lmdeploy。 pip install lmdeploy[all]0.3.…

day77 JSPServlet

知识点&#xff1a; 1Web工程 2JSP是什么&#xff1f;JSP页面包含哪些内容&#xff1f;JSP页面执行原理 3JSP九大内置对象&#xff0c;及四个作用域 4什么是SERVLET&#xff1f;及servlet相关API 5MVC模型 6EL表达式及JSTL标签库的使用 7在JSP页面实现分页和多条件查询 …