C++计算器实现全部功能:双栈表达式求值与错误处理实战

发布时间:2026/10/15 7:08:29
C++计算器实现全部功能:双栈表达式求值与错误处理实战
简介这是一份面向C初学者与进阶学习者的计算器程序完整工程源码基于VC6.0开发环境构建适合用于课程设计、编程练习或面向对象知识巩固。程序实现了加减乘除四则运算并支持撤销输入数字涉及double数据类型、算术运算符、运算符重载、栈结构以及异常处理等核心知识点帮助读者理解类设计与交互式应用开发流程。压缩包共28个文件约283KB包含4个h头文件、3个cpp源文件以及obj、pdb、ilk等编译中间文件另有dsw、dsp工程文件、rc资源脚本、ico图标和exe可执行程序完整保留了从源码到运行结果的工程结构。目前已有600人学习下载。通过研读源码读者可掌握Calculator类的运算符重载实现、基于STL stack的撤销机制、除零校验与输入验证思路并对照工程配置快速编译运行适合作为C面向对象编程的实践参考案例。1. 从零手写 C 计算器为什么“全部功能”四个字最容易翻车很多人看到“C计算器程序 实现了一个计算器的全部功能”这个标题第一反应是“这有什么难的一个 switch 加四则运算两百行搞定”。我当年也这么想直到某次帮一个学弟看他的课设才发现“全部功能”这四个字是个无底洞。他的程序能算12但输入12*3得到 9输入(12)*3直接崩输入1.52输出 3除零直接卡死。这就是典型的“能跑但不算实现全部功能”。所谓“全部功能”落到工程上至少包含四层表达式解析含括号与优先级、浮点数与整数混合运算、错误处理除零、非法字符、括号不匹配、以及一个能持续交互的循环界面。这四层里表达式解析是分水岭——用switch逐字符扫描只能做单步运算要做完整计算器必须上表达式求值算法。适合谁读正在写 C 课设的学生、想补基础语法实战的转行者、以及需要给嵌入式设备写一个轻量计算核心的工程师。下面我按“先能算对、再算稳、最后算快”的顺序把这条路径拆开讲。2. 表达式求值选型双栈法为什么比递归下降更适合课设场景2.1 三种主流方案对比与选型理由实现计算器核心常见做法有三种递归下降、调度场算法双栈、以及直接转后缀表达式再求值。递归下降写起来优雅但对括号嵌套和错误恢复的处理代码量会膨胀调度场算法用两个栈操作符栈 操作数栈边扫描边计算逻辑线性、调试直观转后缀则是调度场的变体多一次遍历但更容易单独测试。我一般会推荐双栈法给课设场景原因是代码量可控在 150 行以内优先级表一目了然出错时能直接打印两个栈的内容定位问题。下面这张表是我实际对比后的结论方案代码量括号支持错误定位难度适合场景递归下降中好中需要扩展函数调用双栈法少好低课设、嵌入式轻量核心转后缀中好中需要多次复用表达式选双栈法还有一个隐性好处它天然支持“边输入边计算”的交互模式不需要先把整行读完再解析这对后面做实时计算器很关键。2.2 双栈法的核心规则与优先级表双栈法的规则可以压缩成四句话遇到数字压操作数栈遇到左括号压操作符栈遇到右括号就弹出操作符直到遇见左括号遇到运算符时只要操作符栈顶优先级不低于当前运算符就先弹出计算再把当前运算符压栈。扫描结束后把剩余操作符全部弹出计算。优先级表是整个算法的“宪法”我通常用两个函数实现而不是硬编码数字这样扩展^或取模时不用改多处// 返回运算符优先级数字越大越优先 int priority(char op) { if (op || op -) return 1; if (op * || op /) return 2; if (op ^) return 3; // 扩展幂运算 return 0; // 括号或非法字符 } // 判断是否为合法运算符 bool isOperator(char c) { return c || c - || c * || c / || c ^; }逻辑说明priority把优先级集中在一处后续加%只需加一行。参数说明传入字符返回整数优先级0 表示不是运算符。注意^是右结合的双栈法默认左结合如果要支持2^3^2需要额外判断这个坑后面避坑章节会讲。2.3 最小可运行版本从字符串到结果下面是一个能算12*3和(12)*3的完整核心函数。为了聚焦算法先假设输入是合法表达式错误处理下一章补。#include iostream #include stack #include string #include cctype double applyOp(double a, double b, char op) { switch (op) { case : return a b; case -: return a - b; case *: return a * b; case /: return a / b; // 除零在调用处拦截 case ^: { double r 1; for (int i 0; i (int)b; i) r * a; return r; } } return 0; } double evaluate(const std::string expr) { std::stackdouble values; std::stackchar ops; for (size_t i 0; i expr.size(); i) { if (isspace(expr[i])) continue; if (isdigit(expr[i]) || expr[i] .) { std::string num; while (i expr.size() (isdigit(expr[i]) || expr[i] .)) num expr[i]; --i; values.push(std::stod(num)); } else if (expr[i] () { ops.push((); } else if (expr[i] )) { while (!ops.empty() ops.top() ! () { double b values.top(); values.pop(); double a values.top(); values.pop(); char op ops.top(); ops.pop(); values.push(applyOp(a, b, op)); } if (!ops.empty()) ops.pop(); // 弹出 ( } else if (isOperator(expr[i])) { while (!ops.empty() priority(ops.top()) priority(expr[i])) { double b values.top(); values.pop(); double a values.top(); values.pop(); char op ops.top(); ops.pop(); values.push(applyOp(a, b, op)); } ops.push(expr[i]); } } while (!ops.empty()) { double b values.top(); values.pop(); double a values.top(); values.pop(); char op ops.top(); ops.pop(); values.push(applyOp(a, b, op)); } return values.top(); }逻辑说明主循环按字符类型分四路处理数字用stod转浮点括号只负责压栈和触发计算运算符按优先级决定是否先算。参数说明expr是待求值字符串返回double。注意--i那行是为了抵消while里多走的一步这是手写扫描器最常见的 off-by-one 点。3. 把“全部功能”补全错误处理、浮点精度与交互循环3.1 除零、非法字符与括号不匹配的拦截点上一版代码遇到1/0会返回inf遇到1会栈空崩溃遇到(12会少弹一次。这三个问题必须在不同位置拦截不能指望一个 try-catch 兜底。除零要在applyOp调用前判断因为浮点除零不抛异常。非法字符在扫描时直接报错并跳过整行。括号不匹配分两种右括号多余时ops里找不到(左括号多余时扫描结束ops里还有(。我通常写一个validate函数先做一遍预检// 返回空字符串表示合法否则返回错误信息 std::string validate(const std::string expr) { int balance 0; for (char c : expr) { if (c () balance; else if (c )) { --balance; if (balance 0) return 右括号多余; } else if (!isdigit(c) !isOperator(c) c ! . !isspace(c)) { return std::string(非法字符: ) c; } } if (balance ! 0) return 左括号未闭合; return ; }逻辑说明balance记录括号净深度一旦为负立即返回避免后续栈操作崩溃。参数说明传入原始表达式返回错误描述或空串。这个函数放在evaluate之前调用能把大部分崩溃转成可读提示。3.2 浮点数比较与输出格式别用 判断结果计算器输出0.10.2得到0.30000000000000004是经典问题。课设里如果要求“显示两位小数”用std::fixed和std::setprecision即可但如果要做“结果是否为整数”的判断绝对不能用。我一般会写一个nearlyEqual辅助函数阈值取1e-9#include cmath #include iomanip bool nearlyEqual(double a, double b, double eps 1e-9) { return std::fabs(a - b) eps; } void printResult(double v) { if (nearlyEqual(v, std::round(v))) std::cout (long long)std::round(v) std::endl; else std::cout std::fixed std::setprecision(6) v std::endl; }逻辑说明先判断是否接近整数是则输出整数形式否则保留 6 位小数。参数说明eps默认1e-9对课设精度足够如果做金融计算需要换成定点数方案那是另一个话题。3.3 交互循环让计算器能连续算而不是算一次就退出一个“全部功能”的计算器必须能持续接收输入直到用户主动退出。这里有个细节用getline而不是cin 否则遇到空格会截断。退出命令我习惯用q或exit。int main() { std::string line; std::cout 简易计算器输入 q 退出\n; while (true) { std::cout ; if (!std::getline(std::cin, line)) break; if (line q || line exit) break; std::string err validate(line); if (!err.empty()) { std::cout 错误: err std::endl; continue; } try { double r evaluate(line); printResult(r); } catch (const std::exception e) { std::cout 计算异常: e.what() std::endl; } } return 0; }逻辑说明getline读整行validate预检evaluate计算异常兜底防止stod抛错。参数说明无。注意try-catch只包evaluate因为validate已经过滤了大部分非法输入异常只可能来自极端数值。4. 避坑与排查双栈计算器最容易踩的五个坑4.1 负数与减号分不清现象输入-35得到错误结果或崩溃。原因扫描到-时一律当二元运算符但开头或左括号后的-是一元负号。解决在扫描时判断-的前一个非空格字符如果是开头、(或另一个运算符就把它当负号处理压一个0进操作数栈再压-或者直接读一个带符号数字。4.2 多位数与小数点被拆开现象输入12.53得到12.53之类的结果。原因数字扫描循环条件写错遇到.后没有继续读后续数字。解决数字扫描用while (i expr.size() (isdigit(expr[i]) || expr[i] .))并且要保证小数点只出现一次可以在循环里加一个bool hasDot标记。4.3 右结合运算符被当左结合现象2^3^2期望512实际得到64。原因双栈法默认就弹出对右结合的^应该用。解决在优先级比较处加一个isRightAssoc判断右结合时只有栈顶优先级严格大于当前才弹出。4.4 括号内为空或只有运算符现象输入()或(3)崩溃。原因validate只检查了括号配对没检查括号内是否有有效操作数。解决在validate里增加状态机记录上一个有效 token 类型左括号后必须是数字或负号右括号前必须是数字或右括号。4.5 大数幂运算溢出与死循环现象输入2^1000000程序卡死。原因applyOp里用循环做幂运算指数过大时循环次数爆炸。解决限制指数范围比如b 1000直接返回inf并提示或者改用std::pow并检查结果是否为inf。5. 进阶技巧把计算器核心抽成可复用类并做单元测试走到这里核心功能已经能跑但代码全堆在main.cpp里下次想复用到别的项目还得复制粘贴。我一般会把核心抽成一个Calculator类把evaluate、validate、applyOp收进去对外只暴露一个double calc(const std::string)接口。这样做的另一个好处是能写单元测试——计算器这种逻辑密集的模块不写测试根本不敢改优先级表。下面是一个极简的测试框架不依赖任何第三方库用断言宏就能覆盖主要分支#include cassert #include iostream void runTests() { Calculator c; assert(c.calc(12) 3); assert(c.calc(12*3) 7); assert(c.calc((12)*3) 9); assert(c.calc(10/4) 2.5); assert(c.calc(-35) 2); assert(c.calc(2^3^2) 512); assert(c.calc(((12)*(34))) 21); std::cout 全部测试通过 std::endl; }逻辑说明每个assert对应一个典型场景覆盖优先级、括号、负数、右结合、嵌套。参数说明无。注意浮点结果不要用直接比这里为了简洁用了整数结果实际项目应换成nearlyEqual。再进一步可以把运算符优先级和结合性做成一张表用std::mapchar, std::pairint, bool存储这样加新运算符只改表不改逻辑。我自己的习惯是任何超过 100 行的解析逻辑先写测试再写实现因为调试栈状态比调试普通变量痛苦得多。最后留一个血泪经验——别在evaluate里直接cout调试信息一旦忘了删输出格式全乱后悔药都没得吃。希望帮到你。本文还有配套的精品资源点击获取