四则运算计算代码实现:递归下降与调度场算法及避坑指南 简介这份资源提供了一套完整的四则运算表达式计算代码面向正在学习编译原理、数据结构或需要自研计算引擎的开发者尤其适合想弄懂括号优先级与递归下降解析的编程学习者。压缩包共4个文件以2个cpp源文件、1个头文件和1个makefile为主源码负责运算逻辑与测试入口头文件声明接口makefile统一编译整体仅3KB结构紧凑便于直接阅读与二次修改。内容围绕加减乘除、括号优先级、递归调用展开并延伸讨论cos、sin、log等三角与对数函数的扩展思路同时涉及除零异常处理与词法、语法分析的基本流程。已有162人学习读者可借此掌握栈或递归处理嵌套括号的方法理解运算符优先级实现细节并参考错误处理机制搭建自己的表达式求值模块。1. 数学公式(四则运算)计算代码从字符串到数值为什么你的解析器总在边界上翻车写业务系统时总有一个需求会突然冒出来用户输入一段(35)*2-4/2你得算出结果。很多人第一反应是找eval但生产环境里没人敢让用户输入直接进eval——那等于把执行权交出去。于是问题变成怎么自己写一个只认四则运算、带括号、能处理小数和负数的计算代码。这件事看起来是编译原理的迷你版实际落地时却卡在几个很具体的地方括号嵌套怎么递归、除零怎么拦、-35里的负号到底算一元还是二元、1.5*2的小数精度怎么保。我见过不少项目在这上面翻车最后用回eval加白名单过滤结果还是被__import__之类的构造绕过。这篇笔记面向的是需要把公式计算嵌进业务里的开发者做报表引擎的、写规则配置的、搞低代码平台的。目标很明确——给你一份能直接抄的四则运算计算代码讲清每一步为什么这么写以及那些只有踩过才知道的坑。2. 两种主流实现路线递归下降和双栈调度场怎么选在动手之前先要把路线定下来。四则运算计算代码的实现方式不止一种选错了后面改起来很痛苦。常见做法是递归下降和双栈调度场两种各有适用场景。2.1 递归下降结构清晰适合要扩展的场景递归下降的核心思路是把表达式按优先级拆成表达式 → 项 → 因子三层每层写一个函数遇到括号就递归回最上层。它的好处是代码结构和文法一一对应后面要加函数调用、幂运算、变量替换直接加一层函数就行。我一般会先写文法expr : term (( | -) term)* term : factor ((* | /) factor)* factor : number | ( expr ) | - factor这个文法里factor的- factor就是处理一元负号的关键它让-35和3*-2都能正确解析。递归下降的代价是函数调用栈会随括号深度增长极端情况下有栈溢出风险但业务场景里括号嵌套超过几十层的几乎不存在。2.2 双栈调度场迭代实现适合超长表达式调度场算法用两个栈——一个存操作数一个存运算符——从左到右扫描遇到运算符就和栈顶比优先级决定是压栈还是弹出计算。它是迭代的没有递归深度问题适合那种一行几千个字符的批量公式。但调度场的代码可读性明显差一截一元负号的判断要额外处理当-出现在表达式开头或左括号后面时它是一元运算符优先级要设得比乘除还高。这个判断写不好(-3)*2就会算错。选型上我的建议很直接业务系统里公式不会太长优先递归下降因为三个月后你还能看懂自己写的代码。只有确实要处理超长表达式、或者运行环境栈空间受限时才上调度场。2.3 一个最小可跑的递归下降实现下面这份代码是 Python 版本去掉注释不到六十行能处理整数、小数、括号、一元负号并且对除零做了拦截。class Parser: def __init__(self, text): self.text text.replace( , ) # 去掉空白简化扫描 self.pos 0 def parse(self): result self.expr() if self.pos ! len(self.text): # 还有剩余字符说明有非法输入 raise ValueError(f非法字符位置 {self.pos}: {self.text[self.pos:]}) return result def expr(self): # 处理加减左结合 value self.term() while self.pos len(self.text) and self.text[self.pos] in -: op self.text[self.pos] self.pos 1 right self.term() if op : value right else: value - right return value def term(self): # 处理乘除左结合 value self.factor() while self.pos len(self.text) and self.text[self.pos] in */: op self.text[self.pos] self.pos 1 right self.factor() if op *: value * right else: if right 0: raise ZeroDivisionError(除数不能为零) value / right return value def factor(self): # 处理数字、括号、一元负号 if self.pos len(self.text): raise ValueError(表达式意外结束) ch self.text[self.pos] if ch (: self.pos 1 value self.expr() if self.pos len(self.text) or self.text[self.pos] ! ): raise ValueError(括号不匹配) self.pos 1 return value if ch -: # 一元负号递归处理后面的因子 self.pos 1 return -self.factor() if ch.isdigit() or ch .: start self.pos while self.pos len(self.text) and (self.text[self.pos].isdigit() or self.text[self.pos] .): self.pos 1 return float(self.text[start:self.pos]) raise ValueError(f无法识别的字符: {ch})逻辑说明parse是入口先调expr拿到结果再检查是否所有字符都被消费掉——这一步很多人会漏导致12abc被静默算成3。expr和term都是标准的左结合循环factor负责最底层的数字、括号和负号。参数说明self.pos是当前扫描位置所有函数共享它这是递归下降的典型写法。数字解析用float如果你需要高精度把float换成decimal.Decimal但要注意Decimal和int混合运算会报错得统一类型。调用方式p Parser((35)*2 - 4/2) print(p.parse()) # 输出 14.03. 把计算代码接进业务输入校验、精度和异常处理能算出结果只是第一步真正上线还要处理用户输入的各种脏数据、小数精度问题以及异常怎么往上抛。3.1 输入校验在解析前先拦一道解析器本身能报错但报错信息对用户不友好。我一般会在解析前加一层预校验用正则快速判断字符集是否合法。import re def validate(expr: str) - bool: # 只允许数字、小数点、四则运算符、括号和空格 if not re.fullmatch(r[\d\.\\-\*/\(\)\s], expr): return False # 括号数量必须匹配 if expr.count(() ! expr.count()): return False return True逻辑说明fullmatch保证整串都符合字符集避免12; rm -rf这种混入。括号计数是最廉价的平衡检查虽然不能发现)(这种顺序错误但能挡掉大部分手误。参数说明正则里的\d包含 Unicode 数字如果只想要 ASCII 数字换成[0-9]。\.转义了点号避免它被当成任意字符。3.2 小数精度float 的坑和 Decimal 的代价0.1 0.2在 float 下等于0.30000000000000004这是二进制浮点的固有问题。如果你的业务是金额计算必须换Decimal。from decimal import Decimal, getcontext getcontext().prec 28 # 设置全局精度 def to_decimal(s): return Decimal(s)逻辑说明Decimal用十进制存储0.1 0.2会精确得到0.3。但代价是运算速度比 float 慢一个数量级而且Decimal(1) 1会抛TypeError必须写成Decimal(1) Decimal(1)。参数说明prec是有效数字位数28 是默认值金额场景一般够用。除法时如果除不尽Decimal会按prec截断需要精确控制的话用quantize指定小数位。3.3 异常处理把解析错误翻译成用户能懂的话解析器抛的ValueError对用户没意义要在业务层包一层。def safe_calc(expr: str): if not validate(expr): return {ok: False, msg: 表达式包含非法字符或括号不匹配} try: result Parser(expr).parse() return {ok: True, value: result} except ZeroDivisionError: return {ok: False, msg: 除数不能为零} except ValueError as e: return {ok: False, msg: f表达式格式错误: {e}}逻辑说明返回结构化结果而不是直接抛异常调用方不用写 try。ZeroDivisionError单独捕获因为它的语义和格式错误不同前端可以给不同提示。参数说明validate先跑能挡掉大部分问题剩下的交给解析器。如果表达式来自不可信来源还要加长度限制防止超长输入拖垮解析。4. 避坑与排查四则运算计算代码最容易翻车的五个地方这一章是我自己踩过的坑每条都按现象、原因、解决来写你对照着查。4.1 现象-35算成-8负号被当成减号原因解析器在expr层看到-就直接当二元减号取了左边的值此时为空去减。根本问题是没区分一元和二元负号。解决在factor层处理一元负号如第 2 章代码所示。判断依据是-出现的位置如果它前面是运算符或左括号或字符串开头就是一元。4.2 现象12)不报错反而算出3原因expr解析完12后pos停在)上但parse没检查剩余字符直接返回了结果。解决parse里必须检查self.pos len(self.text)有剩余就抛错。这个检查成本极低但能挡掉大量脏输入。4.3 现象10/3结果是3小数被吞了原因数字解析时用了int()而不是float()或者两个整数相除时用了整除//。解决统一用float或Decimal。Python 3 里/本身就是浮点除法但如果你从别的语言移植代码要检查是不是写成了整除。4.4 现象括号嵌套深了报递归错误原因递归下降每层括号都会增加调用栈深度Python 默认递归限制是 1000 层。解决业务场景里限制表达式长度和括号深度超过就拒绝。如果确实需要深嵌套改用调度场算法或者手动用栈模拟递归。4.5 现象1.2.3被解析成1.2然后报错位置不对原因数字扫描时遇到第二个.没有停继续往后吃导致float(1.2.3)抛异常但异常信息指向的位置是错的。解决数字扫描时记录小数点出现次数第二次遇到.就停止扫描让后续逻辑报「非法字符」。这样错误位置才准确。5. 进阶技巧用 AST 缓存和运算符优先级表把计算代码做扎实前面给的是能跑的最小实现如果你要把这套代码用在规则引擎或报表里还有两个技巧值得加上。第一个是 AST 缓存。同一个公式可能被反复计算每次都重新解析是浪费。把解析结果存成抽象语法树下次直接对树求值。# 简化的 AST 节点 class Num: def __init__(self, value): self.value value class BinOp: def __init__(self, op, left, right): self.op op self.left left self.right right # 求值函数 def eval_ast(node): if isinstance(node, Num): return node.value left eval_ast(node.left) right eval_ast(node.right) if node.op : return left right if node.op -: return left - right if node.op *: return left * right if node.op /: if right 0: raise ZeroDivisionError return left / right逻辑说明解析阶段把字符串转成树求值阶段只遍历树。树可以缓存变量替换也方便——把Num换成变量节点求值时从上下文取值。参数说明缓存 key 用原始表达式字符串注意去掉空格后再做 key否则12和1 2会存两份。第二个技巧是用优先级表替代硬编码的if op *。把运算符和优先级写进字典expr和term合并成一个循环代码更短加新运算符只改表。PRECEDENCE {: 1, -: 1, *: 2, /: 2} def parse_expr(self, min_prec0): left self.factor() while self.pos len(self.text): op self.text[self.pos] if op not in PRECEDENCE or PRECEDENCE[op] min_prec: break self.pos 1 right self.parse_expr(PRECEDENCE[op] 1) # 左结合用 1 left self.apply(op, left, right) return left逻辑说明min_prec控制当前层能处理的最低优先级遇到更低优先级的运算符就返回上层。左结合靠1实现右结合比如幂运算就不加。参数说明apply是实际计算的函数把除零检查放里面。这个写法比三层函数更紧凑但可读性对新手稍差团队里要统一风格。最后说个我自己的习惯每次写完解析器先拿一组边界用例跑一遍——-35、12)、10/3、((12)*3)、1.2.3、空字符串、只有运算符的串。这七个用例能过基本就稳了。别等上线后用户拿12来试你那时候修的成本高得多。希望帮到你。本文还有配套的精品资源点击获取