从0开始自制解释器——实现多位整数的加减法计算器

news2025/7/13 15:18:09

上一篇我们实现了一个简单的加法计算器,并且了解了基本的词法分析、词法分析器的概念。本篇我们将要对之前实现的加法计算器进行扩展,我们为它添加以下几个功能

  1. 计算减法
  2. 能自动识别并跳过空白字符
  3. 不再局限于单个整数,而是能计算多位整数

提供一些工具函数

首先为了支持减法,我们需要重新定义一下TokenType这个类型,也就是需要给 - 定义一个标志。现在我们的TokenType的定义如下

typedef enum e_TokenType
{
    CINT = 0,
    PLUS,
    MINUS,
    END_OF_FILE
}ETokenType;

由于需要支持多个整数,所以我们也不知道最终会有多少个字符,因此我们提供一个END_OF_FILE 表示我们访问到了最后一个字符,此时应该退出词法分析的过程。

另外因为整数个数不再确定,我们也就不能按照之前的提供一个固定大小的数组。虽然可以提供一个足够大的空间来作为存储数字的缓冲,但是数字少了会浪费空间。而且考虑到之后要支持自定义变量和函数,采用固定长度缓冲的方式就很难找到合适的大小,太大显得浪费空间,太小有时候无法容纳得下用户定义的变量和函数名。因此这里我们采用动态长度的字符缓冲来保存。我们提供一个DyncString 的结构来保存这些内容

#define DEFAULT_BUFFER_SIZE 16

// 动态字符串结构,用于保存任意长度的字符串
typedef struct DyncString
{
    int nLength; // 字符长度
    int capacity; //实际分配的空间大小
    char* pszBuf; //保存字符串的缓冲
}DyncString, *LPDyncString;

// 动态字符串初始化
// str: 被初始化的字符串
// size: 初始化字符串缓冲的大小,如果给0则按照默认大小分配空间
void dyncstring_init(LPDyncString str, int size);
// 动态字符串空间释放
void dyncstring_free(LPDyncString str);
//重分配动态字符串大小
void dyncstring_resize(LPDyncString str, int newSize);
//往动态字符串中添加字符
void dyncstring_catch(LPDyncString str, char c);
// 重置动态数组
void dyncstring_reset(LPDyncString str);

它们的实现如下

/*----------------------------动态数组的操作函数-------------------------------*/
void dyncstring_init(LPDyncString str, int size)
{
    if (NULL == str)
        return;

    if (size == 0)
        str->capacity = DEFAULT_BUFFER_SIZE;
    else
        str->capacity = size;
    str->nLength = 0;
    str->pszBuf = (char*)malloc(sizeof(char) * str->capacity);
    if (NULL == str->pszBuf)
    {
        error("分配内存失败\n");
    }

    memset(str->pszBuf, 0x00, sizeof(char) * str->capacity);
}

void dyncstring_free(LPDyncString str)
{
    if (NULL == str)
        return;

    str->capacity = 0;
    str->nLength = 0;
    if (str->pszBuf == NULL)
        return;

    free(str->pszBuf);
}

void dyncstring_resize(LPDyncString str, int newSize)
{
    int size = str->capacity;
    for (; size < newSize; size = size * 2);
    char* pszStr = (char*)realloc(str->pszBuf, size);
    str->capacity = size;
    str->pszBuf = pszStr;
}

void dyncstring_catch(LPDyncString str, char c)
{
    if (str->capacity == str->nLength + 1)
    {
        dyncstring_resize(str, str->capacity + 1);
    }

    str->pszBuf[str->nLength] = c;
    str->nLength++;
}

void dyncstring_reset(LPDyncString str)
{
    dyncstring_free(str);
    dyncstring_init(str, DEFAULT_BUFFER_SIZE);
}
/*----------------------------End 动态数组的操作函数-------------------------------*/

另外提供一些额外的工具函数,他们的定义如下

void error(char* lpszFmt, ...)
{
    char szBuf[1024] = "";
    va_list arg;
    va_start(arg, lpszFmt);
    vsnprintf(szBuf, 1024, lpszFmt, arg);
    va_end(arg);

    printf(szBuf);
    exit(-1);
}

bool is_digit(char c)
{
    return (c >= '0' && c <= '9');
}
bool is_space(char c)
{
    return (c == ' ' || c == '\t' || c == '\r' || c == '\n');
}

主要算法

我们还是延续之前的算法,一个字符一个字符的解析,只是现在需要额外的将多个整数添加到一块作为一个整数处理。而且需要添加跳过空格的处理。

首先我们对上次的代码进行一定程度的重构。我们添加一个函数专门用来获取下一个字符

char get_next_char()
{
    // 如果到达字符串尾部,索引不再增加
    if (g_pPosition == '\0')
    {
        return '\0';
    }
    else
    {
        char c = *g_pPosition;
        g_pPosition++;
        return c;
    }
}

expr() 函数里面大部分结构不变,主要算法仍然是按次序获取第一个整数、获取算术运算符、获取第二个整数。只是现在的整数都变成了采用 dyncstring 结构来存储

int expr()
{
    int val1 = 0, val2 = 0;
    Token token = { 0 };
    dyncstring_init(&token.value, DEFAULT_BUFFER_SIZE);

    if (get_next_token(&token) && token.type == CINT)
    {
        val1 = atoi(token.value.pszBuf);
    }
    else
    {
        printf("首个操作数必须是整数\n");
        dyncstring_free(&token.value);
        return -1;
    }

    int oper = 0;
    if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS))
    {
        oper = token.type;
    }
    else
    {
        printf("第二个字符必须是操作符, 当前只支持+/-\n");
        dyncstring_free(&token.value);
        return -1;
    }

    if (get_next_token(&token) && token.type == CINT)
    {
        val2 = atoi(token.value.pszBuf);
    }
    else
    {
        printf("操作符后需要跟一个整数\n");
        dyncstring_free(&token.value);
        return -1;
    }

    switch (oper)
    {
    case PLUS:
        {
            printf("%d+%d=%d\n", val1, val2, val1 + val2);
        }
        break;
    case MINUS:
        {
            printf("%d-%d=%d\n", val1, val2, val1 - val2);
        }
        break;
    default:
        printf("未知的操作!\n");
        break;
    }

    dyncstring_free(&token.value);
}

最后就是最终要的 get_next_token 函数了。这个函数最主要的修改就是添加了解析整数和跳过空格的功能

bool get_next_token(LPTOKEN pToken)
{
    char c = get_next_char();

    dyncstring_reset(&pToken->value);
    if (is_digit(c))
    {
        dyncstring_catch(&pToken->value, c);
        pToken->type = CINT;
        parser_number(&pToken->value);
    }
    else if (c == '+')
    {
        pToken->type = PLUS;
        dyncstring_catch(&pToken->value, '+');
    }
    else if (c == '-')
    {
        pToken->type = MINUS;
        dyncstring_catch(&pToken->value, '-');
    }
    else if(is_space(c))
    {
        skip_whitespace();
        return get_next_token(pToken);
    }
    else if ('\0' == c)
    {
        pToken->type = END_OF_FILE;
    }
    else
    {
        return false;
    }
    return true;
}

在这个函数中我们先获取第一个字符,如果字符是整数则获取后面的整数并直接拼接为一个完整的整数。如果是空格则跳过接下来的空格。这两个是可能要处理多个字符所以这里使用了单独的函数来处理。其余只处理单个字符可以直接返回。

parser_numberskip_whitespace 函数比较简单,主要的过程是不断从输入中取出字符,如果是空格则直接将索引往后移动,如果是整数则像对应的整数字符串中将整数字符加入。

void skip_whitespace()
{
    char c = '\0';
    do
    {
        c = get_next_char();
    } while (is_space(c));

    // 遇到不是空白字符的,下次要取用它,这里需要重复取用上次取出的字符
    g_pPosition--;
}

void parser_number(LPDyncString dyncstr)
{
    char c = get_next_char();
    while(is_digit(c))
    {
        dyncstring_catch(dyncstr, c);
        c = get_next_char();
    }

    // 遇到不是数字的,下次要取用它,这里需要重复取用上次取出的字符
    g_pPosition--;
}

唯一需要注意的是,最后都有一个 g_pPosition-- 的操作。因为当我们发现下一个字符不符合条件的时候,它已经过了最后一个数字或者空格了,此时应该已经退回到get_next_token 函数中了,这个函数第一步就是获取下一个字符,因此会产生字符串被跳过的现象。所以这里我们执行 -- 退回到上一个位置,这样再取下一个就不会有问题了。

最后为了能够获取空格的输入,我们将之前的scanf 改成 gets。这样就大功告成了。

我们来测试一下结果
在这里插入图片描述

最后的总结

最后来一个总结。本篇我们对上一次的加法计算器进行了简单的改造,支持加减法、能跳过空格并且能够计算多位整数。

在上一篇文章中,我们提到了Token,并且说过,像 get_next_token 这样给字符串每个部分打上Token的过程就是词法分析。get_next_token 这部分代码可以被称之为词法分析器。这篇我们再来介绍一下其他的概念。

  1. 词位(lexeme):
    词位的中文解释是语言词汇的基本单位。例如汉语的词位是汉字,英语的词位是基本的英文字母。对于我们这个加法计算器来说基本的词位就是数字以及 +\- 这两个符号
  2. parsing(语法分析)和 parser(语法分析器)
    我们所编写的expr函数主要工作流程是根据token来组织代码行为。它的本质就是从Token流中识别出对应的结构,并将结构翻译为具体的行为。例如这里找到的结构是 CINT oper CINT。并且将两个int 按照 oper 指定的运算符进行算术运算。这个将Token流中识别出对应的结构的过程我们称之为语法分析,完成语法分析的组件被称之为语法分析器。expr 函数中即实现了语法分析的功能,也实现了解释执行的功能。

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

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

相关文章

基于轻量级YOLOv5开发构建汉字检测识别分析系统

汉字检测、字母检测、手写数字检测、藏文检测、甲骨文检测在我之前的文章中都有做过了&#xff0c;今天主要是因为实际项目的需要&#xff0c;之前的汉字检测模型较为古老了还使用的yolov3时期的模型&#xff0c;检测精度和推理速度都有不小的滞后了&#xff0c;这里要基于yolo…

rabbitmq集群-普通模式

RabbitMQ的Cluster模式分为两种 普通模式镜像模式 1. 概念解释 1.1 什么是普通模式 普通集群模式&#xff0c;就是将 RabbitMQ 部署到多台服务器上&#xff0c;每个服务器启动一个 RabbitMQ 实例&#xff0c;多个实例之间进行消息通信。 此时我们创建的队列 Queue&#xf…

Android ANR trace日志如何导出

什么是ANR &#xff1f;上网搜索&#xff0c;一搜一大片&#xff0c;我就说个很容易识别的字眼&#xff0c;XXXAPP无响应 ANR trace日志如何导出&#xff1f;使用ADB命令&#xff1a; adb pull data/anr/trace.txt 你要存放的路径。查看ANR报错位置全局搜索你APP的包名&#x…

基于MATLAB的无线信道的传播与衰落(附完整代码与分析)

目录 一. 一般路径损耗模型 1. 1自由环境下路径损耗 1. 2 考虑实际情况 1.3 考虑阴影衰落 二. 代码仿真与理解 &#xff08;1&#xff09;函数文件 &#xff08;2&#xff09;函数文件 &#xff08;3&#xff09;主运行文件 三. 运行结果及理解 3.1 3.2 3.3 一. …

Nacos2.2.0多数据源适配oracle12C-修改Nacos源码

从2.2.0版本开始,可通过SPI机制注入多数据源实现插件,并在引入对应数据源实现后,便可在Nacos启动时通过读取application.properties配置文件中spring.datasource.platform配置项选择加载对应多数据源插件.本文档详细介绍一个多数据源插件如何实现以及如何使其生效。 文章目录一…

机器人运动|浅谈Time Elastic Band算法

前言在自主移动机器人路径规划的学习与开发过程中&#xff0c;我接触到Time Elastic Band算法&#xff0c;并将该算法应用于实际机器人&#xff0c;用于机器人的局部路径规划。在此期间&#xff0c;我也阅读了部分论文、官方文档以及多位大佬的文章&#xff0c;在此对各位大佬的…

git的使用整合

git的下载和安装暂时不论述了&#xff0c;将git安装后会自动配置环境变量&#xff0c;所以环境变量也不需要配置。 一、初始化配置 打开git bash here(使用linux系统下运行的口令)&#xff0c;弹出一个类似于cmd的窗口。 &#xff08;1&#xff09;配置属性 git config --glob…

【LLVM系列】基本文件格式和 CFG 生成指令

一、基本文件格式 参考博客&#xff1a;llvm 文件转换图 .c 文件&#xff0c;c语言源代码。.bc 文件&#xff0c;llvm字节码的二进制形式&#xff08;binary code&#xff09;。.ll 文件&#xff0c;llvm字节码的文本形式。.s 文件&#xff0c;机器汇编代码。.out 文件&#…

操作系统-整理

进程 介绍 进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间&#xff0c;不同进程通过进程间通信来通信。由于进程占据独立的内存&#xff0c;所以上下文进程间的切换开销&#xff08;栈、寄存器、虚拟内存、文件句柄等&#xff09;比较大&#…

第十三章:Java反射机制

第十三章&#xff1a;Java反射机制 13.1&#xff1a;Java反射机制概述 Java Reflection ​ Reflection(反射)是被视为动态语言的关键&#xff0c;反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息&#xff0c;并能直接操作任意对象的内部属性及方法。 ​ 加…

Spring Boot 实现多文件上传

文件上传 Spring Boot代码 代码结构&#xff1a; Controller层 package com.yqifei.upload.controller;import io.swagger.annotations.Api; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile;import javax.serv…

ETL --事实表

每一个事实表通过表的粒度来定义。事实表的粒度是事件度量的定义。我们必须至始至终按照度量如何在 现实世界中理解来规定事实表的粒度。 所有的事实表包含了一组关联到维表的外键&#xff0c;而这些维表提供了事实表度量的上下文。大多数的事实表还 包括了一个或者多个数值型…

小樽C++ 多章⑧ (贰) 指针与数组

目录 1.C中数组变量名某些情况可以看成是指针 2.C语言的scanf 输入语句&#xff0c;printf 输出语句 3.用指针来当动态数组 小樽C 多章⑧ (壹) 指针变量https://blog.csdn.net/weixin_44775255/article/details/129031168 小樽C 多章⑧ (叁) 指针与字符串、(肆) 函数与指针…

GitLab 凭借什么连续 3 年上榜 Gartner 应用程序安全测试魔力象限?听听 GitLab 自己的分析

本文来源&#xff1a;about.gitlab.com 作者&#xff1a;Sandra Gittlen 译者&#xff1a;极狐(GitLab) 市场部内容团队 应用程序安全测试&#xff08;AST&#xff09;对于应用程序研发来说&#xff0c;是一个正在快速发展并且十分重要的领域。DevOps 方法论提到&#xff1a;需…

Java基础之《dubbo(1)—dubbo基础入门》

一、为什么要使用dubbo 1、dubbo是什么 dubbo是一个分布式服务框架&#xff0c;致力于提供高性能和透明化的RPC远程服务调用方案&#xff0c;以及SOA服务治理方案。 2、dubbo有何特点 &#xff08;1&#xff09;远程通讯&#xff1a;提供透明化的远程方法调用&#xff0c;提供…

入门JAVA第十七天 Oracle的JDBC技术

一、数据库JDBC技术学习内容与方法 1.1 学习内容 &#xff08;1&#xff09; Oracle数据库 目前最好的关系型数据库。 基本的CRUD命令 SQL语句。select(R),update(U),delete(D),insert(C) &#xff08;2&#xff09; MySQL数据库 中小型项目非常好用的关系型数据库。 灵活&…

【零基础入门前端系列】—浮动(十八)

【零基础入门前端系列】—浮动&#xff08;十八&#xff09; 一、浮动的定义 float属性定义元素在哪个方向&#xff0c;以往这个属性总应用于图像&#xff0c;使得文本围绕在图像的周围&#xff0c;不过在CSS中&#xff0c;任何元素都可以浮动&#xff0c;浮动的元素会生成一…

【Git】P5 Git 远程仓库(3)pull 发生冲突

pull 发生冲突冲突在什么场景下发生&#xff1f;为什么要先 pull 再 push构建一个冲突场景初始开始操作&#xff1a;程序员2&#xff1a;程序员1&#xff1a;程序员2&#xff1a;发生冲突&#xff1a;查看冲突&#xff1a;解决冲突&#xff1a;冲突在什么场景下发生&#xff1f…

[手写OS]动手实现一个OS 之 准备工作以及引导扇区

[手写OS]动手实现一个OS之第一步-环境以及引导扇区 环境准备 一台可用计算机&#xff08;linux我不知道&#xff0c;我用的Windows&#xff09;汇编编译器NASM一个方便的软盘读写工具VirtualBox 汇编编译器NASM 官网地址&#xff1a;https://www.nasm.us/pub/nasm/snapshot…

java 中的equals()示例代码

Java中的equals()是十分重要的&#xff0c;和要区别开来简述public booleanequals(Object obj)作用&#xff1a;判断其他的对象是否和该对象相等其比较规则为&#xff1a;当参数obj引用的对象与当前对象为同一个对象时&#xff0c;就返回true,否则返回false.简单示例equals()方…