简介这份资源提供了一套完整的数学公式四则运算计算代码面向正在学习编译原理、表达式求值或需要自建计算引擎的开发者与计算机专业学生。代码覆盖加减乘除基础运算并重点处理带括号表达式的优先级问题通过递归调用或栈结构保证先算括号内再算括号外同时兼顾除零异常等错误处理逻辑为后续扩展cos、sin、log等三角函数预留了清晰的实现思路。资源包为tgz格式共4个文件包含2个cpp源文件、1个头文件与1个makefile分别承担核心计算逻辑、接口声明与编译构建职责整体约3KB结构紧凑便于直接阅读与二次开发。目前已有162人学习下载适合希望理解词法分析、语法分析与递归下降解析器实现细节的读者参考也可作为课程设计或小型计算器项目的起步代码。1. 四则运算计算代码从字符串到数值一份能直接跑通的实现拆解做表单校验、报表公式、配置化计算规则的时候绕不开一个需求给一串3 5 * (2 - 8) / 4这样的字符串算出结果。很多人第一反应是eval()但线上环境里eval是黑匣子用户输入__import__(os).system(...)这种内容就是灾难。所以真正能上生产的路子只有一条自己写词法分析加语法分析把中缀表达式转成后缀表达式再求值。这份四则运算计算代码就是干这个的它不依赖任何第三方库纯逻辑实现适合需要把计算能力嵌进业务系统、又不想引入重型表达式引擎的开发者。加减乘除四个运算符、括号优先级、多位数、小数、负数这些边界它都覆盖了。下面我按实际拆包的顺序把这份代码的结构、跑法、参数和坑一条条讲清楚。2. 核心原理中缀转后缀与双栈求值2.1 为什么不能直接从左到右算四则运算的难点不在加减乘除本身而在优先级和括号。3 5 * 2如果从左往右算先算3 5得 8再乘 2 得 16正确答案是 13。这就是优先级问题。括号更麻烦(3 5) * 2要求先算括号内的。人眼能一眼看出该先算哪部分但程序不行程序需要一个明确的规则来决定运算顺序。常见做法是先把中缀表达式就是我们平时写的3 5 * 2转成后缀表达式也叫逆波兰表达式写成3 5 2 * 后缀表达式的特点是运算符在操作数后面而且没有括号计算时从左到右扫描遇到数字压栈遇到运算符就弹出两个操作数算完再压回去。这样优先级和括号的问题在转换阶段就解决了求值阶段变得极其简单。转换用的算法叫调度场算法Shunting Yard核心是一个运算符栈和一个输出队列。规则不复杂遇到数字直接输出遇到运算符如果栈顶运算符优先级不低于当前运算符就先把栈顶弹出来输出再把当前运算符压栈遇到左括号直接压栈遇到右括号就不断弹出栈顶输出直到遇到左括号左括号弹出但不输出。扫描完后把栈里剩余的运算符全部弹出输出。2.2 双栈求值的具体流程后缀表达式求值用的是一个操作数栈。扫描后缀表达式遇到数字就压栈遇到运算符就弹出栈顶两个数注意先弹出的是右操作数后弹出的是左操作数做完运算把结果压回去。最后栈里剩下的那个数就是结果。这里有个容易翻车的点减法和除法的操作数顺序。5 - 3在后缀表达式里是5 3 -扫描到-时先弹出 3再弹出 5必须用后弹出的减先弹出的也就是5 - 3。如果写反了变成3 - 5结果就是 -2。除法同理10 / 2弹出 2 和 10要算10 / 2而不是2 / 10。这个顺序问题在代码里就一行的事但调试的时候如果没意识到会怀疑人生。下面是我一般会用的核心实现Python 版本逻辑清晰方便移植到其他语言def tokenize(expr): 把表达式字符串拆成 token 列表支持多位数和小数 tokens [] i 0 while i len(expr): ch expr[i] if ch.isspace(): i 1 continue if ch.isdigit() or ch .: # 处理多位数和小数点比如 3.14 或 100 num while i len(expr) and (expr[i].isdigit() or expr[i] .): num expr[i] i 1 tokens.append((num, float(num))) continue if ch in -*/(): tokens.append((op, ch)) i 1 continue raise ValueError(f非法字符: {ch}) return tokens def infix_to_postfix(tokens): 调度场算法中缀 token 列表转后缀 token 列表 precedence {: 1, -: 1, *: 2, /: 2} output [] op_stack [] for typ, val in tokens: if typ num: output.append((typ, val)) elif val (: op_stack.append(val) elif val ): while op_stack and op_stack[-1] ! (: output.append((op, op_stack.pop())) if not op_stack: raise ValueError(括号不匹配) op_stack.pop() # 弹出左括号不输出 else: # 当前运算符优先级不高于栈顶时先弹出栈顶 while (op_stack and op_stack[-1] ! ( and precedence[op_stack[-1]] precedence[val]): output.append((op, op_stack.pop())) op_stack.append(val) while op_stack: top op_stack.pop() if top (: raise ValueError(括号不匹配) output.append((op, top)) return output def eval_postfix(postfix): 后缀表达式求值 stack [] for typ, val in postfix: if typ num: stack.append(val) else: if len(stack) 2: raise ValueError(表达式不完整) right stack.pop() left stack.pop() if val : stack.append(left right) elif val -: stack.append(left - right) elif val *: stack.append(left * right) elif val /: if right 0: raise ValueError(除数不能为 0) stack.append(left / right) if len(stack) ! 1: raise ValueError(表达式有误) return stack[0] def calculate(expr): 对外统一入口 tokens tokenize(expr) postfix infix_to_postfix(tokens) return eval_postfix(postfix)这段代码分三层tokenize负责词法分析把字符串拆成有类型的 tokeninfix_to_postfix负责语法转换用调度场算法处理优先级和括号eval_postfix负责求值用操作数栈算出最终结果。三层分开的好处是每层可以单独测试出问题的时候能快速定位是词法、语法还是求值阶段的问题。参数方面precedence字典定义了运算符优先级加减是 1乘除是 2。如果要扩展取模%或者幂运算**在这里加一行就行。tokenize里对数字的处理支持多位数和小数点但没处理负号因为负号在四则运算里既可以是一元运算符表示负数也可以是二元运算符表示减法需要额外判断上下文。如果输入里可能出现-3 5这种需要在 tokenize 阶段判断-前面是不是运算符或左括号或开头如果是就当一元负号处理。3. 跑通与验证从命令行到单元测试3.1 最小可运行验证代码拿到手第一件事是跑几个用例确认环境没问题。不需要装任何依赖Python 3.6 以上直接跑python3 -c from calc import calculate print(calculate(3 5 * 2)) # 13.0 print(calculate((3 5) * 2)) # 16.0 print(calculate(10 / 4)) # 2.5 print(calculate(2 * (3 4) - 5)) # 9.0 如果这四个用例都输出正确说明核心逻辑没问题。注意10 / 4在 Python 3 里是浮点除法结果是 2.5如果业务需要整数除法得在eval_postfix里把/改成//但那样10 / 3会变成 3 而不是 3.333看具体场景选。3.2 边界用例与单元测试上面四个用例只覆盖了正常路径真正容易出问题的是边界情况。我一般会写一组测试用例覆盖空表达式、括号不匹配、除零、多位数、小数、连续运算符这些场景import pytest from calc import calculate def test_basic(): assert calculate(1 2) 3 assert calculate(10 - 3) 7 assert calculate(4 * 5) 20 assert calculate(20 / 4) 5 def test_precedence(): assert calculate(2 3 * 4) 14 assert calculate(2 * 3 4) 10 assert calculate(10 - 2 * 3) 4 def test_parentheses(): assert calculate((2 3) * 4) 20 assert calculate(((1 2) * (3 4))) 21 def test_decimal(): assert abs(calculate(0.1 0.2) - 0.3) 1e-9 assert calculate(3.5 * 2) 7.0 def test_errors(): with pytest.raises(ValueError): calculate(1 ) with pytest.raises(ValueError): calculate((1 2) with pytest.raises(ValueError): calculate(1 / 0) with pytest.raises(ValueError): calculate(1 a)跑pytest test_calc.py -v就能看到每个用例的结果。这里有个细节0.1 0.2在浮点数里不等于 0.3而是 0.30000000000000004所以断言里用了abs(...) 1e-9而不是直接。如果业务对精度要求高比如财务计算得用decimal.Decimal替换float但那样性能会下降看场景权衡。3.3 性能与扩展考量单次计算的开销主要在 tokenize 和两次栈操作上对于长度 100 以内的表达式耗时在微秒级别完全够用。如果要在高并发场景下反复计算同一个表达式可以把infix_to_postfix的结果缓存起来只对变量部分重新求值。比如报表场景里公式结构固定只有单元格引用在变这时候缓存后缀表达式能省掉转换开销。扩展方面加变量支持只需要在 tokenize 里识别标识符在 eval_postfix 里从上下文取值。加函数支持比如sin、max需要在 tokenize 里识别函数名在调度场算法里把函数也当运算符处理但优先级要单独定义。这些扩展都不需要改动核心的三层结构加对应的分支就行。4. 避坑与排查那些让我加班到凌晨的细节4.1 浮点数精度问题现象calculate(0.1 0.2)返回0.30000000000000004用户看到这个结果会认为程序有 bug。原因IEEE 754 浮点数表示法无法精确表示 0.1 和 0.2相加后产生舍入误差。这不是代码的问题是所有用浮点数的语言都有的问题。解决如果业务允许微小误差在输出时做四舍五入比如round(result, 10)。如果业务要求精确比如金额把float换成decimal.Decimal在 tokenize 阶段用Decimal(num)构造运算全程用 Decimal最后按需转回 float 或字符串。4.2 括号不匹配导致死循环现象输入((1 2)时程序卡住或者抛出奇怪的异常。原因调度场算法里遇到右括号时会不断弹出栈顶直到遇到左括号。如果左括号不存在op_stack会被弹空while循环的条件如果写得不严谨就会出问题。另外扫描结束后如果栈里还有左括号说明右括号少了也需要报错。解决在弹出循环里加op_stack非空判断弹出后如果栈空且没遇到左括号直接抛ValueError(括号不匹配)。扫描结束后检查栈里是否有残留的左括号有就报错。上面代码里这两处都做了处理。4.3 减法和除法的操作数顺序现象calculate(5 - 3)返回-2calculate(10 / 2)返回0.2。原因后缀表达式求值时先弹出的是右操作数后弹出的是左操作数。如果代码里写成stack.append(left - right)但实际left和right的赋值反了就会算成3 - 5和2 / 10。解决严格保证right stack.pop()在前left stack.pop()在后运算时用left op right。这个顺序在加法和乘法里无所谓但减法和除法里是致命的。我一般会在代码里加注释标明避免后来维护的人改错。4.4 空表达式和纯空格输入现象输入空字符串或者只有空格时tokenize返回空列表后续eval_postfix里stack为空stack[0]抛IndexError。原因没有对空输入做前置校验。解决在calculate入口处判断tokens是否为空为空直接抛ValueError(表达式为空)。或者在eval_postfix最后判断len(stack) ! 1时统一处理。两种方式都行入口处处理更清晰。4.5 连续运算符的处理现象输入1 2或1 * 2时tokenize 能通过但求值阶段会出错。原因词法分析只负责拆 token不负责校验语法合法性。1 2会被拆成1、、、2调度场算法能处理但求值时第二个弹出时栈里只有一个操作数抛ValueError(表达式不完整)。解决如果希望给出更明确的错误提示可以在 tokenize 之后加一个语法校验步骤检查运算符和操作数的交替是否合法。但大多数场景下求值阶段的报错已经够用不需要额外做完整的语法分析。5. 进阶技巧把计算代码嵌进业务系统的三种姿势第一种姿势是直接调用。把calculate函数作为工具方法暴露给业务层表单提交时校验用户输入的公式配置化报表里解析计算公式。这种用法最简单适合计算频率不高、表达式不长的场景。注意对用户输入做长度限制防止恶意构造超长表达式导致栈溢出。第二种姿势是预编译加缓存。如果同一个表达式会被反复计算比如报表模板里的固定公式把infix_to_postfix的结果缓存起来每次只跑eval_postfix。缓存 key 用表达式字符串value 用后缀 token 列表。这样省掉了 tokenize 和转换的开销在高频场景下能明显提升吞吐。我一般会用functools.lru_cache装饰转换函数几行代码就能搞定。第三种姿势是扩展成表达式引擎。在现有三层结构上增加变量、函数、条件表达式支持变成一个轻量级的规则引擎。变量支持在 tokenize 里识别标识符求值时从上下文字典取值函数支持在调度场算法里把函数名当运算符处理求值时调用对应的 Python 函数条件表达式可以引入三元运算符? :优先级设得比加减还低。这些扩展不需要改动核心逻辑加分支就行。验证扩展后的引擎是否正确我习惯用对比测试同一组表达式分别用扩展引擎和 Python 的eval跑仅在测试环境用eval结果一致就说明逻辑没问题。注意eval只用来做测试对照绝对不能出现在生产代码里。从那以后我每次把计算代码嵌进业务系统之前都会强制走一遍边界用例测试特别是除零、括号不匹配和浮点精度这三项确认报错信息对用户友好、不会泄露内部堆栈。希望帮到你。本文还有配套的精品资源点击获取