从0开始自制解释器实现多位整数的加减法计算器
上一篇我们实现了一个简单的加法计算器,并且了解了基本的词法分析、词法分析器的概念。本篇我们将要对之前实现的加法计算器进行扩展,我们为它添加以下几个功能 计算减法 能自动识别并跳过空白字符 不再局限于单个整数,而是能计算多位整数 提供一些工具函数
首先为了支持减法,我们需要重新定义一下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("分配内存失败 "); } 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 == " " || c == "r" || c == " "); } 主要算法
我们还是延续之前的算法,一个字符一个字符的解析,只是现在需要额外的将多个整数添加到一块作为一个整数处理。而且需要添加跳过空格的处理。
首先我们对上次的代码进行一定程度的重构。我们添加一个函数专门用来获取下一个字符 char get_next_char() { // 如果到达字符串尾部,索引不再增加 if (g_pPosition == " ") { return " "; } 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("首个操作数必须是整数 "); 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("第二个字符必须是操作符, 当前只支持+/- "); dyncstring_free(&token.value); return -1; } if (get_next_token(&token) && token.type == CINT) { val2 = atoi(token.value.pszBuf); } else { printf("操作符后需要跟一个整数 "); dyncstring_free(&token.value); return -1; } switch (oper) { case PLUS: { printf("%d+%d=%d ", val1, val2, val1 + val2); } break; case MINUS: { printf("%d-%d=%d ", val1, val2, val1 - val2); } break; default: printf("未知的操作! "); 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 (" " == c) { pToken->type = END_OF_FILE; } else { return false; } return true; }
在这个函数中我们先获取第一个字符,如果字符是整数则获取后面的整数并直接拼接为一个完整的整数。如果是空格则跳过接下来的空格。这两个是可能要处理多个字符所以这里使用了单独的函数来处理。其余只处理单个字符可以直接返回。
parser_number 和 skip_whitespace 函数比较简单,主要的过程是不断从输入中取出字符,如果是空格则直接将索引往后移动,如果是整数则像对应的整数字符串中将整数字符加入。void skip_whitespace() { char c = " "; 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 这部分代码可以被称之为词法分析器。这篇我们再来介绍一下其他的概念。词位(lexeme):
词位的中文解释是语言词汇的基本单位。例如汉语的词位是汉字,英语的词位是基本的英文字母。对于我们这个加法计算器来说基本的词位就是数字以及 +- 这两个符号parsing(语法分析)和 parser(语法分析器)
我们所编写的expr函数主要工作流程是根据token来组织代码行为。它的本质就是从Token流中识别出对应的结构,并将结构翻译为具体的行为。例如这里找到的结构是 CINT oper CINT 。并且将两个int 按照 oper 指定的运算符进行算术运算。这个将Token流中识别出对应的结构的过程我们称之为语法分析,完成语法分析的组件被称之为语法分析器。expr 函数中即实现了语法分析的功能,也实现了解释执行的功能。
现代青年的精神内耗,靠读懂国际风云缓解金鳞岂是池中物,一遇风云便化龙2022年10月,又是一个极其重要的月份,决定了中国乃至世界的命运。但现在很多年轻人是不关心国际政治和国内大事的,他们觉得这些事都太大也太远,跟自己每
野菊花山东新融媒体中心三天静默疫情来得让人猝不及防儿子在房间里自习妻子在厨房里做饭我上完网课,坐在院子里沐浴着秋天的阳光天上的白云像一只只任性的绵羊在天空里啃着草儿牧羊的儿童一定躺在草地
从化温泉度假区珍稀氡温泉入住游记头条创作挑战赛卓思道温泉酒店地处有广州后花园之称的从化良口镇流溪温泉度假区,原生态森林环抱,融合徽派建筑和岭南文化于一体的简约中式风格设计。地址广州市从化区良口镇御泉大道288号温
连江奇达村旗冠顶头条创作挑战赛奇达村旗冠顶位于福州市连江县安凯乡的白云山山顶,因形似战旗而得名。登临山顶便可一睹海上牧场的壮观景色。导航旗冠顶,石碑处就是登山点,也可以继续开车到山上。需登上1,8
凭什么呀人家都是靠自己努力赚来的请大家不要妄自菲薄嘴下留情我就不明白了,56岁大妈一定要弄成满脸皱纹蓬乱的头发花哨的大棉袄,才行吗?不能化妆不能戴耳钉不能烫头?呼和浩特李少莉作为政府官员,漂亮出镜不好吗?她56岁了,还能保持30岁容貌,真
曾经努力过才不会后悔人世间所有真正值得我们欣喜若狂的成功和美好的事物,都不是轻而易举触手可得的。太容易得到的东西和实现的目标,由于缺乏时间的沉淀,缺乏情感的投入,也不会在我们的内心产生轰鸣,不会带来更
戈登火箭的表现好了许多我努力顶防乔治但有时只能接受现实直播吧11月1日讯火箭队今日常规赛9395遗憾不敌快船队,快船队由乔治完成绝杀。当时,乔治在埃里克戈登的贴身盯防下,高难度翻身跳投命中。赛后,埃里克戈登接受采访时说道我们的表现好了
记得国羽女神王适娴吗?嫁给谌龙后生1娃,安置到体育局薪资可观自羽毛球运动诞生以来,其战术打法比赛规则就在不断地完善和变化,如今的羽毛球运动,是一项对抗性非常强的竞技类体育运动,比赛实行21分制,与之前的15分制相比,对运动员的体能要求更高,
字母哥317霍勒迪25710雄鹿擒活塞豪取6连胜NBA常规赛11月1日继续进行,最终,雄鹿以110108战胜活塞,雄鹿开赛6连胜!首节开始,雄鹿接连冲击内线得分打出114开局!各拿4分后洛佩兹和波蒂斯连中三分合力打出一波80继续
NBA111成功狙击奇才哈登23717恩比德缺赛北京时间11月1日,202223赛季NBA常规赛继续进行,费城76人客场挑战华盛顿奇才。全场打完,76人118111险胜奇才,拿到三连胜。首节,76人队的恩比德休战,梅尔顿进入首发
特斯拉再度降价,中国车企能不能跟?我简直要被特斯拉逼疯了。这是一名特斯拉车主的真实反应。他在朋友圈中发文说前几天他还沉浸在拥有特斯拉的兴奋中,过了几天就像是吃了苍蝇一样难受。这位用户遭遇了什么呢?根据特斯拉10月二
哈姆我不想再看到最后两分钟的裁判报告了!直播吧1月29日讯今日NBA常规赛,湖人客场加时121125不敌绿军,湖人主帅哈姆赛后接受了媒体采访。常规时间最后时刻,詹姆斯直杀内线一打五突破不进,詹姆斯控诉塔图姆打手犯规但裁判
节后开工忙全国各地招聘活动火热启动节后开工忙,宁夏山东江苏等多地的各类招聘活动火热启动。宁夏就业援助月活动启动组织300余场招聘会宁夏银川昨天(1月31日)启动2023春风行动就业援助月专项服务活动。逾百家企业展开
节前花生强势收尾节后关注进口到港受商品花生米节日需求提振,国产花生春节前收尾价格普遍较2022年12月的低点反弹200400元吨。尽管节前油厂相继停收停机,但由于产区现货供应始终有限,春节节日效应带来的食用需求增
1月28日猪价回落,春节后下跌魔咒没有被打破!今天是1月28日,农历正月初七,春节假期已结束,节后上班的第1天,生猪价格开始回落,下跌的魔咒没有被打破!昨天生猪平均价格为7。95元斤,实际成交价格在7。5元斤,比去年同期每斤价
58安居客研究院春节期间二手房市场呈现北热南冷中证网讯(记者董添)1月28日,58安居客研究院发布春节期间楼市成交数据。统计数据显示,2023年春节期间,全国70城二手房春节期间日均需求热度相比1月以来均值出现明显上涨。58安
张学良4子1女结局,三个嫡子无一人善终,私生子在美国不会说中文来源丨小姐姐讲史张学良一共有过三段婚姻,他的大姐发妻子于凤至给他生下了3子1女他的第二任妻子随军夫人谷瑞玉没有给他生任何子女他的小妹第三任夫人赵一荻,为他生下最小的儿子,张学良一共
坚定信心,推动中国经济航船驶向更光明未来玉兔伴休闲,旅游迎新春。7天的春节假期,既是难得的团聚时间,也是休闲游玩需求高涨的黄金周。伴随疫情防控举措优化调整,这个春节假期,中远程旅游稳步复苏,旅行体验年味浓厚。飞猪数据显示
新田县举行春风行动暨产业开发区专场招聘会红网时刻新闻2月1日讯(通讯员蒋若冰)春节过后,新田县各重大项目企业陆续开工,为满足企业的用工需求,实现首季开门红,1月30日,新田县人社局抢抓春节期间外出务工人员集中返乡和流动人
陪着小孩玩一玩,才知道人之初,性本善原来是顶漂亮的帽子先说观感,人之初,性本善原本就是顶人工编织的帽子,漂亮好看又好听。实际观察一下,没它说得那么动听,居中,不善又不恶。这也只是偶然发现,在陪小朋友玩耍过程中,有些小体会。春节陪三四岁
跟孩子讲道理,不如陪孩子看电影!陪孩子看过很多亲子电影后,我一直有个感想跟孩子讲道理不如带孩子看电影,每部电影都在用温和有趣多元的方式在跟孩子讲道理,能够帮助孩子更好地成长和塑造健康的价值观人生观。话不多说,开始
这是我见过最美的母女瑜伽,太有爱了!练瑜伽的母女搭档之前也有分享过给大家每每看到母女一起瑜伽画面嘴角都会不自觉的上扬妥妥变成姨母笑今天这对瑜伽母女来自新加坡妈妈名字叫Jimin,女儿叫SarahJimin想要通过自己