C++拓展:(一)计算器实现:从中缀表达式到逆波兰表达式
前言
在日常编程学习和开发中,计算器是一个经典且极具代表性的项目。它不仅能帮助我们巩固数据结构中的栈应用,还能深入理解表达式求值的核心逻辑。我们平时书写的数学表达式,如
1-2*(3-4)+5,都是中缀表达式 —— 运算符位于两个运算数之间。但这种表达式的直接求值存在一个关键问题:运算符优先级和括号的处理。例如,在上述表达式中,遇到-和*时无法直接运算,因为括号(3-4)的优先级更高。为了解决这个问题,计算机科学中常采用 “中缀表达式转后缀表达式(逆波兰表达式)” 的思路。逆波兰表达式(Reverse Polish Notation, RPN)由波兰逻辑学家 J・卢卡西维兹于 1929 年提出,其核心特点是运算符位于运算数之后,且天然包含了运算优先级信息,无需额外处理括号和运算符优先级对比,求值过程简洁高效。
本文将详细讲解 C++ 计算器的完整实现过程,从逆波兰表达式的求值、中缀表达式到后缀表达式的转换,再到空格处理、负数处理等边界情况的解决,最终实现一个支持加减乘除和括号运算的完整计算器。下面就让我们正式开始吧!
一、核心原理
在开始代码实现之前,我们需要先明确两个核心步骤:逆波兰表达式的求值过程,以及中缀表达式转后缀表达式的转换规则。这两个步骤是计算器实现的基石,理解其原理是后续代码编写的关键。
1.1 逆波兰表达式(后缀表达式)求值原理
逆波兰表达式的求值过程依赖栈这种数据结构,利用栈 “后进先出” 的特性,能够快速完成运算。其核心逻辑如下:
- 建立一个栈,用于存储运算数;
- 依次遍历逆波兰表达式中的每个元素;
- 如果当前元素是运算数,直接将其入栈;
- 如果当前元素是运算符,弹出栈顶的两个运算数(注意:先弹出的是右运算数,后弹出的是左运算数);
- 使用当前运算符对两个运算数进行计算,将结果作为新的运算数入栈;
- 遍历完表达式后,栈中剩余的唯一元素就是最终的运算结果。
举个简单的例子:逆波兰表达式1 2 + 3 4 * -(对应的中缀表达式为1+2-(3*4))的求值过程:
- 遍历到
1,入栈,栈:[1];- 遍历到
2,入栈,栈:[1,2];- 遍历到
+,弹出 2(右)和 1(左),计算 1+2=3,入栈,栈:[3];- 遍历到
3,入栈,栈:[3,3];- 遍历到
4,入栈,栈:[3,3,4];- 遍历到
*,弹出 4(右)和 3(左),计算 3*4=12,入栈,栈:[3,12];- 遍历到
-,弹出 12(右)和 3(左),计算 3-12=-9,入栈,栈:[-9];- 遍历结束,结果为 - 9。
这个过程是无需考虑运算符优先级的,因为逆波兰表达式已经在转换过程中处理了优先级和括号,求值逻辑非常直观。
1.2 中缀表达式转后缀表达式原理
中缀表达式转后缀表达式是计算器实现的核心难点,其关键在于利用栈处理运算符的优先级和括号。转换规则如下:
1.2.1 转换规则
- 建立一个栈,用于存储运算符;
- 依次遍历中缀表达式中的每个字符;
- 如果当前字符是运算数(0-9),直接输出到后缀表达式中;
- 如果当前字符是左括号
(,直接入栈; - 如果当前字符是右括号
),则弹出栈顶运算符并输出,直到遇到左括号(为止(左括号弹出但不输出); - 如果当前字符是运算符(+、-、*、/):
- 若栈为空,或栈顶元素是左括号
(,则当前运算符直接入栈; - 若当前运算符的优先级高于栈顶运算符,直接入栈;
- 若当前运算符的优先级低于或等于栈顶运算符,则弹出栈顶运算符并输出,重复此过程,直到栈为空或栈顶运算符优先级低于当前运算符,最后将当前运算符入栈;
- 若栈为空,或栈顶元素是左括号
- 遍历完中缀表达式后,弹出栈中剩余的所有运算符,依次输出到后缀表达式中。


1.2.2 运算符优先级定义
为了判断运算符优先级,我们需要明确:*和/的优先级高于+和-。本文定义优先级如下:
+、-:优先级 1;*、/:优先级 2。
1.2.3 转换案例
这里以中缀表达式1+2-(3*4+5)-7为例,为大家详细展示转换过程:
| 遍历字符 | 栈状态(运算符) | 后缀表达式 | 说明 |
|---|---|---|---|
| 1 | 空 | 1 | 运算数,直接输出 |
| + | [+] | 1 | 栈空,入栈 |
| 2 | [+] | 1 2 | 运算数,直接输出 |
| - | [-] | 1 2 + | -优先级等于栈顶+,弹出+输出,栈空后入栈- |
| ( | [-, (] | 1 2 + | 左括号,直接入栈 |
| 3 | [-, (] | 1 2 + 3 | 运算数,直接输出 |
| * | [-, (, *] | 1 2 + 3 | 栈顶是(,入栈* |
| 4 | [-, (, *] | 1 2 + 3 4 | 运算数,直接输出 |
| + | [-, (, +] | 1 2 + 3 4 * | +优先级低于栈顶*,弹出*输出,栈顶是(,入栈+ |
| 5 | [-, (, +] | 1 2 + 3 4 * 5 | 运算数,直接输出 |
| ) | [-] | 1 2 + 3 4 * 5 + | 右括号,弹出运算符直到(,(弹出不输出 |
| - | [-] | 1 2 + 3 4 * 5 + - | -优先级等于栈顶-,弹出-输出,栈空后入栈- |
| 7 | [-] | 1 2 + 3 4 * 5 + - 7 | 运算数,直接输出 |
| 遍历结束 | 空 | 1 2 + 3 4 * 5 + - 7 - | 弹出栈中剩余-,输出 |
最终得到的后缀表达式为1 2 + 3 4 * 5 + - 7 -,对应的计算结果为1+2-(3*4+5)-7 = 3 - 17 -7 = -21,后续求值过程将验证这一结果。
二、逆波兰表达式求值实现(evalRPN)
2.1 功能说明
实现一个函数evalRPN,输入为逆波兰表达式的字符串数组(每个元素为运算数或运算符),输出为计算结果。
2.2 代码实现
#include <iostream>
#include <vector>
#include <stack>
#include <string>
#include <cassert>
using namespace std;class Calculator {
public:// 逆波兰表达式求值int evalRPN(const vector<string>& tokens) {stack<int> numStack; // 存储运算数的栈for (size_t i = 0; i < tokens.size(); ++i) {const string& token = tokens[i];// 判断当前token是运算数还是运算符if (isOperator(token)) {// 运算符:弹出两个运算数计算assert(numStack.size() >= 2 && "逆波兰表达式格式错误,运算符数量过多");int rightNum = numStack.top();numStack.pop();int leftNum = numStack.top();numStack.pop();// 根据运算符计算结果并入栈int result = calculate(leftNum, rightNum, token[0]);numStack.push(result);} else {// 运算数:转换为int后入栈numStack.push(stoi(token));}}assert(numStack.size() == 1 && "逆波兰表达式格式错误,运算数数量过多");return numStack.top();}private:// 判断字符串是否为运算符(+、-、*、/)bool isOperator(const string& s) {return s == "+" || s == "-" || s == "*" || s == "/";}// 执行二元运算int calculate(int left, int right, char op) {switch (op) {case '+':return left + right;case '-':return left - right;case '*':return left * right;case '/':// 注意:这里默认除数不为0,实际应用中需添加除数为0的判断assert(right != 0 && "除数不能为0");return left / right;default:assert(false && "不支持的运算符");return 0;}}
};// 测试函数
void testEvalRPN() {Calculator calc;// 测试用例1:1+2-(3*4+5)-7 = -21vector<string> tokens1 = {"1", "2", "+", "3", "4", "*", "5", "+", "-", "7", "-"};assert(calc.evalRPN(tokens1) == -21);// 测试用例2:3*(4-5/2) = 3*(4-2) = 6vector<string> tokens2 = {"3", "4", "5", "2", "/", "-", "*"};assert(calc.evalRPN(tokens2) == 6);// 测试用例3:100/25 = 4vector<string> tokens3 = {"100", "25", "/"};assert(calc.evalRPN(tokens3) == 4);cout << "逆波兰表达式求值测试全部通过!" << endl;
}int main() {testEvalRPN();return 0;
}2.3 代码解析
- 数据结构选择:使用
stack<int>存储运算数,利用栈的后进先出特性快速获取最近的两个运算数; - 运算符判断:通过
isOperator函数判断当前字符串是否为支持的运算符(+、-、*、/); - 运算逻辑:当遇到运算符时,弹出栈顶两个元素,注意先弹出的是右运算数,后弹出的是左运算数(例如
a - b,栈中先入 a 再入 b,弹出时先得到 b,再得到 a,计算 a - b); - 异常处理:使用
assert断言处理逆波兰表达式格式错误(如运算符过多、运算数过多)和除数为 0 的情况。
2.4 运行结果
编译运行上述代码,最终得到输出如下:
逆波兰表达式求值测试全部通过!说明逆波兰表达式的求值逻辑正确,能够处理各种合法的逆波兰表达式。
三、中缀表达式转后缀表达式实现(toRPN)
3.1 功能说明
实现一个函数toRPN,输入为中缀表达式字符串,输出为对应的逆波兰表达式字符串数组。需要处理的主要问题有:运算符优先级对比、括号的递归处理、多位数的读取。
3.2 代码实现
#include <iostream>
#include <vector>
#include <stack>
#include <string>
#include <cctype>
#include <cassert>
using namespace std;class Calculator {
public:// 中缀表达式转后缀表达式vector<string> toRPN(const string& infix) {vector<string> rpn;stack<char> opStack; // 存储运算符的栈size_t i = 0;int n = infix.size();while (i < n) {if (isdigit(infix[i])) {// 读取多位数string numStr;while (i < n && isdigit(infix[i])) {numStr += infix[i];++i;}rpn.push_back(numStr);} else if (infix[i] == '(') {// 左括号:入栈opStack.push(infix[i]);++i;} else if (infix[i] == ')') {// 右括号:弹出运算符直到左括号while (!opStack.empty() && opStack.top() != '(') {rpn.push_back(string(1, opStack.top()));opStack.pop();}assert(!opStack.empty() && "中缀表达式格式错误,括号不匹配");opStack.pop(); // 弹出左括号(不输出)++i;} else if (isSupportedOperator(infix[i])) {// 运算符:处理优先级while (!opStack.empty() && opStack.top() != '(' && getPriority(infix[i]) <= getPriority(opStack.top())) {// 当前运算符优先级低于或等于栈顶,弹出栈顶运算符rpn.push_back(string(1, opStack.top()));opStack.pop();}opStack.push(infix[i]);++i;} else {// 非法字符assert(false && "中缀表达式包含非法字符");++i;}}// 弹出栈中剩余的所有运算符while (!opStack.empty()) {assert(opStack.top() != '(' && "中缀表达式格式错误,括号不匹配");rpn.push_back(string(1, opStack.top()));opStack.pop();}return rpn;}private:// 判断是否为支持的运算符bool isSupportedOperator(char c) {return c == '+' || c == '-' || c == '*' || c == '/';}// 获取运算符优先级int getPriority(char op) {switch (op) {case '+':case '-':return 1;case '*':case '/':return 2;default:assert(false && "不支持的运算符");return 0;}}
};// 打印逆波兰表达式
void printRPN(const vector<string>& rpn) {for (size_t i = 0; i < rpn.size(); ++i) {if (i > 0) {cout << " ";}cout << rpn[i];}cout << endl;
}// 测试函数
void testToRPN() {Calculator calc;// 测试用例1:1+2-(3*4+5)-7string infix1 = "1+2-(3*4+5)-7";vector<string> rpn1 = calc.toRPN(infix1);vector<string> expected1 = {"1", "2", "+", "3", "4", "*", "5", "+", "-", "7", "-"};assert(rpn1 == expected1);cout << "中缀表达式:" << infix1 << endl;cout << "后缀表达式:";printRPN(rpn1);// 测试用例2:3*(4-5/2)string infix2 = "3*(4-5/2)";vector<string> rpn2 = calc.toRPN(infix2);vector<string> expected2 = {"3", "4", "5", "2", "/", "-", "*"};assert(rpn2 == expected2);cout << "中缀表达式:" << infix2 << endl;cout << "后缀表达式:";printRPN(rpn2);// 测试用例3:100/25+3*6string infix3 = "100/25+3*6";vector<string> rpn3 = calc.toRPN(infix3);vector<string> expected3 = {"100", "25", "/", "3", "6", "*", "+"};assert(rpn3 == expected3);cout << "中缀表达式:" << infix3 << endl;cout << "后缀表达式:";printRPN(rpn3);// 测试用例4:(1+2)*(3-4)/5string infix4 = "(1+2)*(3-4)/5";vector<string> rpn4 = calc.toRPN(infix4);vector<string> expected4 = {"1", "2", "+", "3", "4", "-", "*", "5", "/"};assert(rpn4 == expected4);cout << "中缀表达式:" << infix4 << endl;cout << "后缀表达式:";printRPN(rpn4);cout << "\n中缀表达式转后缀表达式测试全部通过!" << endl;
}int main() {testToRPN();return 0;
}3.3 代码解析
- 多位数处理:当遇到数字字符时,持续读取后续字符直到非数字,组成完整的运算数(如 “123” 不会被拆分为 “1”“2”“3”);
- 括号处理:左括号直接入栈,右括号触发栈中运算符弹出,直到遇到左括号(左括号弹出不输出),确保括号内的子表达式优先处理;
- 优先级对比:通过
getPriority函数返回运算符优先级,当前运算符优先级低于或等于栈顶运算符时,弹出栈顶运算符输出,保证高优先级运算符先入后缀表达式; - 非法字符检测:使用
assert断言处理非法字符和括号不匹配的情况,增强了代码的健壮性。
3.4 运行结果
编译运行上述代码,得到输出结果如下:
中缀表达式:1+2-(3*4+5)-7
后缀表达式:1 2 + 3 4 * 5 + - 7 -
中缀表达式:3*(4-5/2)
后缀表达式:3 4 5 2 / - *
中缀表达式:100/25+3*6
后缀表达式:100 25 / 3 6 * +
中缀表达式:(1+2)*(3-4)/5
后缀表达式:1 2 + 3 4 - * 5 /中缀表达式转后缀表达式测试全部通过!可见转换结果与预期一致,说明中缀转后缀的逻辑正确,能够处理各种复杂的中缀表达式。
四、完整计算器实现:处理边界情况
4.1 边界情况说明
前面的实现已经完成了计算器的核心功能,但实际使用中还会遇到其他的边界情况,即需要额外处理以下的情况:
- 空格处理:中缀表达式中可能包含空格(如
1 + 2 * 3),需要先去除所有空格,否则会影响字符识别; - 负数处理:中缀表达式中可能出现负数(如
-1+2、3*(-4)),需要区分 “减号” 和 “负号”,将负数-x转换为0-x,避免解析错误。
4.2 完整代码实现
#include <iostream>
#include <vector>
#include <stack>
#include <string>
#include <cctype>
#include <cassert>
#include <algorithm>
using namespace std;class Calculator {
public:// 完整计算器入口:输入中缀表达式字符串,返回计算结果int calculate(string s) {// 步骤1:去除所有空格string cleaned = removeSpaces(s);// 步骤2:处理负数(将-x转换为0-x)string processed = processNegativeNumbers(cleaned);// 步骤3:中缀表达式转后缀表达式vector<string> rpn = toRPN(processed);// 步骤4:后缀表达式求值return evalRPN(rpn);}private:// 步骤1:去除字符串中的所有空格string removeSpaces(const string& s) {string res;res.reserve(s.size()); // 预分配空间,提升效率for (char c : s) {if (c != ' ') {res += c;}}return res;}// 步骤2:处理负数,将-x转换为0-xstring processNegativeNumbers(const string& s) {string res;int n = s.size();for (size_t i = 0; i < n; ++i) {if (s[i] == '-') {// 负号的情况:1. 字符串开头;2. 前一个字符不是数字且不是右括号if (i == 0 || (!isdigit(s[i-1]) && s[i-1] != ')')) {res += "0-";} else {// 减号,直接添加res += '-';}++i;} else {res += s[i];}}return res;}// 步骤3:中缀表达式转后缀表达式(复用第三节实现,优化括号处理)vector<string> toRPN(const string& infix) {vector<string> rpn;stack<char> opStack;size_t i = 0;int n = infix.size();while (i < n) {if (isdigit(infix[i])) {// 读取多位数string numStr;while (i < n && isdigit(infix[i])) {numStr += infix[i];++i;}rpn.push_back(numStr);} else if (infix[i] == '(') {opStack.push(infix[i]);++i;} else if (infix[i] == ')') {// 弹出运算符直到左括号while (!opStack.empty() && opStack.top() != '(') {rpn.push_back(string(1, opStack.top()));opStack.pop();}assert(!opStack.empty() && "括号不匹配(缺少左括号)");opStack.pop(); // 弹出左括号++i;} else if (isSupportedOperator(infix[i])) {// 处理运算符优先级while (!opStack.empty() && opStack.top() != '(' && getPriority(infix[i]) <= getPriority(opStack.top())) {rpn.push_back(string(1, opStack.top()));opStack.pop();}opStack.push(infix[i]);++i;} else {assert(false && "非法字符");++i;}}// 弹出剩余运算符while (!opStack.empty()) {assert(opStack.top() != '(' && "括号不匹配(缺少右括号)");rpn.push_back(string(1, opStack.top()));opStack.pop();}return rpn;}// 步骤4:逆波兰表达式求值(复用第二节实现,优化异常处理)int evalRPN(const vector<string>& tokens) {stack<int> numStack;for (const string& token : tokens) {if (isOperator(token)) {assert(numStack.size() >= 2 && "表达式格式错误");int right = numStack.top();numStack.pop();int left = numStack.top();numStack.pop();numStack.push(calculate(left, right, token[0]));} else {numStack.push(stoi(token));}}assert(numStack.size() == 1 && "表达式格式错误");return numStack.top();}// 辅助函数:判断是否为支持的运算符(字符)bool isSupportedOperator(char c) {return c == '+' || c == '-' || c == '*' || c == '/';}// 辅助函数:判断是否为支持的运算符(字符串)bool isOperator(const string& s) {return s == "+" || s == "-" || s == "*" || s == "/";}// 辅助函数:获取运算符优先级int getPriority(char op) {switch (op) {case '+':case '-':return 1;case '*':case '/':return 2;default:assert(false && "不支持的运算符");return 0;}}// 辅助函数:执行二元运算int calculate(int left, int right, char op) {switch (op) {case '+':return left + right;case '-':return left - right;case '*':return left * right;case '/':assert(right != 0 && "除数不能为0");return left / right;default:assert(false && "不支持的运算符");return 0;}}
};// 测试函数
void testCalculator() {Calculator calc;// 测试用例1:带空格和负数:1 + 2 * (-3) = 1 -6 = -5string expr1 = "1 + 2 * (-3)";assert(calc.calculate(expr1) == -5);cout << expr1 << " = " << calc.calculate(expr1) << endl;// 测试用例2:纯负数:-10 + (-20)/5 = -10 -4 = -14string expr2 = "-10 + (-20)/5";assert(calc.calculate(expr2) == -14);cout << expr2 << " = " << calc.calculate(expr2) << endl;// 测试用例3:复杂混合运算:(10 - 2*3) / (2 + 1) = (10-6)/3 = 4/3 = 1string expr3 = "(10 - 2*3) / (2 + 1)";assert(calc.calculate(expr3) == 1);cout << expr3 << " = " << calc.calculate(expr3) << endl;// 测试用例4:括号嵌套和负数:3*(4 - (-5)/2) = 3*(4+2) = 18string expr4 = "3*(4 - (-5)/2)";assert(calc.calculate(expr4) == 18);cout << expr4 << " = " << calc.calculate(expr4) << endl;// 测试用例5:无空格纯数字:123+456*789 = 123 + 359784 = 359907string expr5 = "123+456*789";assert(calc.calculate(expr5) == 359907);cout << expr5 << " = " << calc.calculate(expr5) << endl;// 测试用例6:除数为0(触发断言)// string expr6 = "5/0";// calc.calculate(expr6);// 测试用例7:括号不匹配(触发断言)// string expr7 = "(1+2";// calc.calculate(expr7);cout << "\n所有测试用例执行通过!" << endl;
}int main() {testCalculator();return 0;
}4.3 代码优化解析
4.3.1 空格处理(removeSpaces 函数)
- 功能:遍历输入字符串,过滤掉所有空格字符,返回无空格字符串;
- 优化:使用
res.reserve(s.size())预分配空间,避免字符串拼接过程中的频繁扩容,提升效率; - 示例:输入
"1 + 2 * 3",输出"1+2*3"。
4.3.2 负数处理(processNegativeNumbers 函数)
- 核心逻辑:区分 “减号” 和 “负号”:
- 负号场景:字符串开头(如
-1+2)、前一个字符不是数字且不是右括号(如3*(-4));- 处理方式:将
-x替换为0-x,例如-1变为0-1,-4变为0-4,确保后续转换和求值逻辑正常处理;
4.3.3 代码复用与整合
- 复用了
evalRPN函数和toRPN函数,仅做了少量优化(使用了范围 for 循环简化代码); - 新增辅助函数
isSupportedOperator(判断字符是否为运算符)和isOperator(判断字符串是否为运算符),职责划分更清晰; - 异常处理:通过
assert断言处理括号不匹配、除数为 0、表达式格式错误等问题,在实际项目中可替换为try-catch异常处理,提供更友好的错误提示。例如:// 自定义异常类 class CalculatorException : public exception { public:explicit CalculatorException(const string& msg) : msg_(msg) {}const char* what() const noexcept override {return msg_.c_str();} private:string msg_; };// 除数为0异常 class DivideByZeroException : public CalculatorException { public:DivideByZeroException() : CalculatorException("除数不能为0") {} };// 括号不匹配异常 class MismatchedParenthesesException : public CalculatorException { public:MismatchedParenthesesException(const string& msg) : CalculatorException(msg) {} };// 修改calculate函数,抛出异常 double calculate(double left, double right, char op) {switch (op) {case '/':if (right == 0) {throw DivideByZeroException();}return left / right;// 其他运算符...} }// 在main函数中捕获异常 int main() {Calculator calc;try {string expr = "5/0";cout << calc.calculate(expr) << endl;} catch (const CalculatorException& e) {cout << "错误:" << e.what() << endl;}return 0; }
4.4 运行结果
编译运行上述代码,得到输出结果如下:
1 + 2 * (-3) = -5
-10 + (-20)/5 = -14
(10 - 2*3) / (2 + 1) = 1
3*(4 - (-5)/2) = 18
123+456*789 = 359907所有测试用例执行通过!所有测试用例均通过,可见我们设计出的计算器能够正确处理空格、负数、括号嵌套、多位数等复杂场景,计算结果是准确的。
总结
本计算器的实现不仅巩固了栈的数据结构应用,还涉及字符串处理、异常处理、模块化设计等多个 C++ 核心知识点。通过扩展功能(如支持浮点数、更多运算符)和优化性能,还可以进一步提升计算器的实用性和效率。
希望本文能帮助大家深入理解表达式求值的核心逻辑,为后续更复杂的编程项目打下坚实基础。如果在实现过程中遇到问题,欢迎在评论区交流讨论!
