实战:简易计算器(栈+后缀表达式)
说实话,计算器这个题目,几乎是每个学编程的人都会遇到的经典。但很多人写出来的计算器只能处理两个数加减乘除,一碰到括号就懵了。我记得我刚开始工作那会儿,接了个需求要做一个表达式求值的小工具,当时我天真地以为用递归硬解析就行,结果被各种括号嵌套和运算符优先级折磨得够呛。
后来我才意识到,真正优雅的做法,是用后缀表达式(也叫逆波兰表达式)配合栈来解决。今天我们就来手写一个这样的简易计算器。
为什么是后缀表达式?
我们平时写的表达式叫中缀表达式,比如 3 + 5 * (2 - 1)。人眼一看就知道先算括号里的,再算乘法,最后加法。但计算机不是这样思考的——它需要一个明确的、没有歧义的计算顺序。
后缀表达式的写法是把运算符放在操作数后面。比如 3 + 4 写成 3 4 +,3 + 5 * (2 - 1) 写成 3 5 2 1 - * +。你想想看,这种写法完全不需要括号,也不需要优先级规则,计算机只需要从左到右扫描,遇到操作数就压栈,遇到运算符就弹出两个数计算,再把结果压回去。多清爽。
核心思想:中缀表达式是人友好的,后缀表达式是机器友好的。我们的计算器要做的事,就是把中缀转成后缀,然后求值。
整体流程
整个计算器分为两大步:
- 中缀转后缀——用栈处理运算符和括号
- 后缀求值——用栈处理操作数
下面这张图可以帮你理清思路:
第一步:中缀转后缀
这一步需要一个栈来临时存放运算符。规则其实不复杂,我总结成几句话:
- 遇到数字,直接输出到后缀表达式
- 遇到左括号
(,压栈 - 遇到右括号
),不断弹出栈顶运算符并输出,直到遇到左括号(左括号不输出) - 遇到运算符(
+ - * /),弹出栈中所有优先级大于等于当前运算符的运算符并输出,然后把当前运算符压栈 - 扫描结束后,把栈中剩余的运算符全部弹出
嗯,这里要注意优先级:* 和 / 优先级高于 + 和 -。我曾经在项目里因为优先级判断写反了,导致表达式 1 + 2 * 3 算出来是 9 而不是 7,排查了半天才发现是这里的问题。
小技巧:可以用一个函数 int precedence(char op) 返回运算符的优先级,比如 + 和 - 返回 1,* 和 / 返回 2。这样比较起来非常清晰。
第二步:后缀表达式求值
这一步就简单多了。你只需要一个操作数栈:
- 从左到右扫描后缀表达式
- 遇到数字,压栈
- 遇到运算符,弹出栈顶两个数字(注意顺序:先弹出的是右操作数,后弹出的是左操作数),计算结果再压回去
- 扫描结束后,栈顶就是最终结果
举个例子,表达式 3 5 2 1 - * +:
| 扫描到的元素 | 操作 | 栈内容(从底到顶) |
|---|---|---|
| 3 | 压栈 | 3 |
| 5 | 压栈 | 3, 5 |
| 2 | 压栈 | 3, 5, 2 |
| 1 | 压栈 | 3, 5, 2, 1 |
| - | 弹出 2 和 1,计算 2-1=1,压栈 | 3, 5, 1 |
| * | 弹出 5 和 1,计算 5*1=5,压栈 | 3, 5 |
| + | 弹出 3 和 5,计算 3+5=8,压栈 | 8 |
结果就是 8,完美。
完整代码实现
下面是我写的一个完整实现。我个人习惯把中缀转后缀和后缀求值分成两个函数,这样逻辑更清晰,也方便测试。
#include <iostream>
#include <stack>
#include <string>
#include <sstream>
#include <cctype>
#include <stdexcept>
using namespace std;
// 返回运算符优先级
int precedence(char op) {
if (op == '+' || op == '-') return 1;
if (op == '*' || op == '/') return 2;
return 0;
}
// 中缀表达式转后缀表达式
string infixToPostfix(const string& infix) {
stack<char> ops;
string postfix;
istringstream iss(infix);
char ch;
while (iss >> ch) {
if (isdigit(ch)) {
// 处理多位数字
postfix += ch;
while (iss.peek() != EOF && isdigit(iss.peek())) {
iss >> ch;
postfix += ch;
}
postfix += ' ';
} else if (ch == '(') {
ops.push(ch);
} else if (ch == ')') {
while (!ops.empty() && ops.top() != '(') {
postfix += ops.top();
postfix += ' ';
ops.pop();
}
if (ops.empty() || ops.top() != '(') {
throw runtime_error("括号不匹配");
}
ops.pop(); // 弹出 '('
} else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
while (!ops.empty() && precedence(ops.top()) >= precedence(ch)) {
postfix += ops.top();
postfix += ' ';
ops.pop();
}
ops.push(ch);
} else {
// 忽略空格或其他字符
continue;
}
}
while (!ops.empty()) {
if (ops.top() == '(') {
throw runtime_error("括号不匹配");
}
postfix += ops.top();
postfix += ' ';
ops.pop();
}
return postfix;
}
// 计算后缀表达式
int evaluatePostfix(const string& postfix) {
stack<int> st;
istringstream iss(postfix);
string token;
while (iss >> token) {
if (isdigit(token[0])) {
st.push(stoi(token));
} else if (token == "+" || token == "-" || token == "*" || token == "/") {
if (st.size() < 2) {
throw runtime_error("表达式错误:操作数不足");
}
int right = st.top(); st.pop();
int left = st.top(); st.pop();
int result;
switch (token[0]) {
case '+': result = left + right; break;
case '-': result = left - right; break;
case '*': result = left * right; break;
case '/':
if (right == 0) throw runtime_error("除数不能为0");
result = left / right;
break;
default: throw runtime_error("未知运算符");
}
st.push(result);
} else {
throw runtime_error("非法token: " + token);
}
}
if (st.size() != 1) {
throw runtime_error("表达式错误:操作数多余");
}
return st.top();
}
int main() {
string infix;
cout << "请输入中缀表达式(数字和运算符之间用空格分隔): ";
getline(cin, infix);
try {
string postfix = infixToPostfix(infix);
cout << "后缀表达式: " << postfix << endl;
int result = evaluatePostfix(postfix);
cout << "计算结果: " << result << endl;
} catch (const exception& e) {
cerr << "错误: " << e.what() << endl;
}
return 0;
}
避坑指南:我曾经在写后缀求值时,弹出操作数的顺序搞反了。减法 left - right 如果写成 right - left,结果就完全错了。记住:先弹出的是右操作数,后弹出的是左操作数。
扩展思考
这个计算器目前只支持整数和四则运算。如果你想支持浮点数、幂运算、甚至三角函数,思路是一样的:
- 在词法分析阶段识别更多类型的 token
- 在优先级判断时加入新的运算符
- 在求值阶段实现对应的计算逻辑
说白了,框架搭好了,剩下的就是往里填功能。我后来在公司做的那个表达式引擎,就是在这个基础上一步步扩展出来的,支持了变量、函数调用、甚至条件表达式。但核心始终没变——栈加后缀表达式。
总结一下:中缀转后缀用运算符栈,后缀求值用操作数栈。两个栈,两个步骤,搞定一个计算器。这个模式在很多场景下都适用,比如编译器里的表达式解析、配置文件中的公式计算等等。学会了它,你以后遇到类似的问题,心里就有底了。
公众号:蓝海资料掘金营,微信deep3321