当前位置: 首页 > news >正文

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

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

  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;elsestr->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 函数中即实现了语法分析的功能,也实现了解释执行的功能。

相关文章:

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

上一篇我们实现了一个简单的加法计算器&#xff0c;并且了解了基本的词法分析、词法分析器的概念。本篇我们将要对之前实现的加法计算器进行扩展&#xff0c;我们为它添加以下几个功能 计算减法能自动识别并跳过空白字符不再局限于单个整数&#xff0c;而是能计算多位整数 提…...

(12)C#传智:File类,泛型,字典,FileStream,StreamReader,多态

内容有点多&#xff0c;重点:泛型、字典&#xff0c;流与多态。 继续深入学习内容&#xff1a;List、Dictionary、using语句、FileStream 一、File类的继续学心 File.ReadAllLines(string path,Encoding,encoding)指定编码读取返回行字串数组 File.WriteAllText(string…...

Dubbo的服务暴漏与服务发现源码详解

服务暴漏 如果配置需要刷新则根据配置优先级刷新服务配置 如果服务已经导出&#xff0c;则直接返回 是否异步导出&#xff08;全局或者服务级别配置了异步&#xff0c;则需要异步导出服务&#xff09; 服务暴漏入口DefaultModuleDeployer#exportServices private void exp…...

Python 的IDE——PyCharm

IDE介绍与安装 介绍 集成开发环境&#xff08;IDE&#xff09; 集成开发环境(IDE,integrated Development Environment) —— 集成开发软件需要的所有工具&#xff0c;一般包括以下工具&#xff1a; 图形用户界面 代码编辑器(支持代码补全、自动缩进) 编译器/解释器 调试器…...

01 C语言使用链表实现队列(Queue、FIFO)模块

01 C语言使用链表实现队列&#xff08;Queue、FIFO&#xff09;模块 作者将狼才鲸创建日期2023-03-08Gitee源码仓库地址&#xff1a;C语言使用链表实现队列&#xff08;Queue、FIFO&#xff09;模块 Linux原生的队列KFIFO一次只能操作一个队列&#xff0c;操作变长元素时&…...

2.2操作系统-进程管理:前趋图、前趋图与PV操作

2.1操作系统-进程管理&#xff1a;前趋图\前趋图与PV操作前趋图前趋图与PV操作练习前趋图与PV操作&#xff0c;一般出现了&#xff0c;分值在2~3分左右&#xff0c;技巧性很强。 前趋图 前趋图是为了描述一个程序的各部分间的依赖关系&#xff0c;或者是一个大的计算的各个子…...

凤凰游攻略

凤凰游攻略1 装备&#x1f4e6;1.1 证件1.2 日常用品1.3 药品1.4 衣物1.5 洗漱用品2 交通&#x1f697;3 住宿&#x1f3e0;4 美食&#x1f355;5 拍照&#x1f4f7;5.1 租苗族服5.1.1 单租服装5.1.2 服装化妆5.2 一条龙旅拍6 路线&#x1f5fa;️景点&#x1f3d9;️7 注意⚠️…...

Nginx 高可用方案

准备工作 10.10.4.5 10.10.4.6 VIP&#xff1a;10.10.4.10 两台虚拟机。安装好Nginx 安装Nginx 更新yum源文件&#xff1a; rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm wget -O /etc/yum.repos.d/CentOS-Ba…...

Linux基本指令

文章目录 常用Linux命令常见Linux指令 1、ls指令 语法&#xff1a;ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。常用选项&#xff1a; -a 列出目录下的所有文件&#xf…...

Linux系统基础命令(二)

一、浏览和切换目录 ls命令&#xff1a;列出文件和目录&#xff0c;主要用于列出文件和目录 CentOS的终端默认是有颜色标注的。一般来说&#xff1a;蓝色--->目录&#xff1b;绿色-->可执行文件&#xff1b;红色--->压缩文件&#xff1b;浅蓝色--->链接文件&#…...

【C++】C++11——简介|列表初始|简化声明|nullptr与范围for|STL中的变化

文章目录一、C11简介二、列表初始化三、简化声明四、nullptr与范围for五、STL中一些变化一、C11简介 在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1)&#xff0c;使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。不过由于TC1主要是对C98标准中的漏洞进行修复…...

Python -- 函数

文章目录1、一个简单的函数2、多参数函数3、返回值3.1、简单的返回3.2、返回列表和字典4、传入列表5、传入任意数量的实参5.1、以元组和字典的形式5.2、形参的排列顺序6、将函数储存在模块中1、一个简单的函数 函数用关键字def来定义&#xff0c;传参时不用指定参数类型 para&…...

Pytorch中utils.data 与torchvision简介

Pytorch中utils.data 与torchvision简介1 数据处理工具概述2 utils.data简介3 torchvision简介3.1 transforms3.2 ImageFolder1 数据处理工具概述 Pytorch涉及数据处理&#xff08;数据装载、数据预处理、数据增强等&#xff09;主要工具包及相互关系如下图所示&#xff0c;主…...

学习 Python 之 Pygame 开发魂斗罗(十)

学习 Python 之 Pygame 开发魂斗罗&#xff08;十&#xff09;继续编写魂斗罗1. 解决敌人不开火的问题2. 创建爆炸效果类3. 为敌人跳入河中增加爆炸效果4. 玩家击中敌人继续编写魂斗罗 在上次的博客学习 Python 之 Pygame 开发魂斗罗&#xff08;九&#xff09;中&#xff0c;…...

Keepalive+LVS群集部署

KeepaliveLVS群集部署一、Keepalive概述1、什么是Keepalive2、Keepalive工作原理3、Keepalive主要模块及作用4、Keepalived 服务重要功能&#xff08;1&#xff09;管理 LVS 负载均衡软件&#xff08;2&#xff09;支持故障自动切换&#xff08;3&#xff09;实现 LVS 负载调度…...

数组、指针总结【面试题】

文章目录0. 补充知识数组笔试题1. 一维数组1.1 字符数组1.1.1 sizeof1.1.2 strlen1.2 二维数组2. 指针笔试题0. 补充知识 在进入数组与指针的练习时&#xff0c;我们先来复习以下以下的知识点&#xff0c;这可以帮助我们更好的理解下面练习 数组是一组能存放相同类型的类型的元…...

七色电子标签

机种名 电子会议桌牌 型号 ESL_7color_7.3_D 外观尺寸 176.2x137.15x80mm 产品重量 268g 可视区域 163.297.92mm 外观颜色 银色 供电方式 锂电池供电2300mAh&#xff08;Type-C 接口可充电&#xff09; 显示技术 E-INK电子纸&#xff0c;双屏 像素 800x480 像…...

大数据是什么?发展前景怎么样

关于大数据的解释&#xff0c;比较官方的定义是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合&#xff0c;是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产。简单来说&#xff0c;大数据就是结构化…...

MYSQL必知必会 | 查询相关

汇总数据 聚集函数 有时只需要汇总数据&#xff0c;并不需要把数据实际检索出来&#xff0c;所以MySql提供了专门的函数 聚集函数&#xff1a;运行在行组上&#xff0c;计算和返回单个值的函数 函数说明AVG()返回某列平均值COUNT()返回某列的行数MAX()返回某列最大值MIN()返…...

Java学习环境一站说明(保姆级详细教学)

1.Java开发环境搭建官网下载www.oracle.com2.安装注意&#xff1a;1.选择安装位置时尽量不要安装到C盘&#xff0c;路径中不要有空格以及中文的存在2.开发人员安装的jdk中包含了jre&#xff0c;所以不需要单独安装jre3.环境变量配置打开高级系统设置2.点击环境变量3.在系统变量…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

centos 7 部署awstats 网站访问检测

一、基础环境准备&#xff08;两种安装方式都要做&#xff09; bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats&#xff0…...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

Python如何给视频添加音频和字幕

在Python中&#xff0c;给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加&#xff0c;包括必要的代码示例和详细解释。 环境准备 在开始之前&#xff0c;需要安装以下Python库&#xff1a;…...

3403. 从盒子中找出字典序最大的字符串 I

3403. 从盒子中找出字典序最大的字符串 I 题目链接&#xff1a;3403. 从盒子中找出字典序最大的字符串 I 代码如下&#xff1a; class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

听写流程自动化实践,轻量级教育辅助

随着智能教育工具的发展&#xff0c;越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式&#xff0c;也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建&#xff0c;…...

Pinocchio 库详解及其在足式机器人上的应用

Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库&#xff0c;专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性&#xff0c;并提供了一个通用的框架&…...