实战:简易计算器(栈+后缀表达式)

说实话,计算器这个题目,几乎是每个学编程的人都会遇到的经典。但很多人写出来的计算器只能处理两个数加减乘除,一碰到括号就懵了。我记得我刚开始工作那会儿,接了个需求要做一个表达式求值的小工具,当时我天真地以为用递归硬解析就行,结果被各种括号嵌套和运算符优先级折磨得够呛。

后来我才意识到,真正优雅的做法,是用后缀表达式(也叫逆波兰表达式)配合来解决。今天我们就来手写一个这样的简易计算器。

为什么是后缀表达式?

我们平时写的表达式叫中缀表达式,比如 3 + 5 * (2 - 1)。人眼一看就知道先算括号里的,再算乘法,最后加法。但计算机不是这样思考的——它需要一个明确的、没有歧义的计算顺序。

后缀表达式的写法是把运算符放在操作数后面。比如 3 + 4 写成 3 4 +3 + 5 * (2 - 1) 写成 3 5 2 1 - * +。你想想看,这种写法完全不需要括号,也不需要优先级规则,计算机只需要从左到右扫描,遇到操作数就压栈,遇到运算符就弹出两个数计算,再把结果压回去。多清爽。

核心思想:中缀表达式是人友好的,后缀表达式是机器友好的。我们的计算器要做的事,就是把中缀转成后缀,然后求值。

整体流程

整个计算器分为两大步:

  1. 中缀转后缀——用栈处理运算符和括号
  2. 后缀求值——用栈处理操作数

下面这张图可以帮你理清思路:

简易计算器核心流程 输入中缀表达式 步骤1:中缀转后缀(运算符栈) 步骤2:后缀表达式求值(操作数栈) 输出计算结果

第一步:中缀转后缀

这一步需要一个栈来临时存放运算符。规则其实不复杂,我总结成几句话:

  • 遇到数字,直接输出到后缀表达式
  • 遇到左括号 (,压栈
  • 遇到右括号 ),不断弹出栈顶运算符并输出,直到遇到左括号(左括号不输出)
  • 遇到运算符(+ - * /),弹出栈中所有优先级大于等于当前运算符的运算符并输出,然后把当前运算符压栈
  • 扫描结束后,把栈中剩余的运算符全部弹出

嗯,这里要注意优先级:*/ 优先级高于 +-。我曾经在项目里因为优先级判断写反了,导致表达式 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