实现一个支持四则运算的计算器程序,首先需要明确程序的整体架构和数据处理流程。计算器程序的核心任务是将用户输入的数学表达式字符串转换为可计算的结果,这个过程涉及词法分析、语法解析和表达式求值三个阶段。在C++中,我们通常采用面向对象的设计方法,将计算器封装成一个独立的类,这样既保证了代码的模块化,也便于后续功能扩展。

一、计算器程序的整体设计思路
从数据结构角度来看,计算器程序最关键的是如何正确处理运算符的优先级问题。比如表达式3+5*2,如果不考虑优先级直接从左到右计算,会得到错误的结果16,而正确答案应该是13。为了解决这个问题,业界普遍采用双栈方案:一个栈用于存储操作数,另一个栈用于存储操作符。通过比较栈顶操作符与当前操作符的优先级,决定是否立即执行运算,这种机制能够完美处理加减乘除的优先级关系。
在设计计算器类时,我们需要考虑几个核心成员:输入字符串的存储与预处理、操作数栈和操作符栈的管理、运算符优先级的定义表,以及主计算循环。此外还需要处理一些边界情况,比如除数为零、括号不匹配、非法字符输入等异常场景。良好的错误处理机制能够提升程序的健壮性,避免运行时崩溃。
整个计算流程可以概括为:遍历表达式字符串,遇到数字时压入操作数栈,遇到运算符时先与栈顶运算符比较优先级,如果当前运算符优先级较低则先执行栈顶运算,然后再将当前运算符入栈。当表达式遍历结束后,依次弹出操作符栈中剩余的运算符并执行运算,最终操作数栈中剩下的唯一值就是计算结果。
二、词法分析与表达式解析
词法分析是计算器程序的第一步,负责将原始的字符串表达式拆解为一个个有意义的词法单元。比如输入字符串"12+34*5",词法分析器需要将其拆分为数字12、运算符+、数字34、运算符*、数字5这五个词法单元。在C++中实现词法分析器,最简单的方法是逐字符遍历字符串,根据字符类型进行分类处理。
数字解析是词法分析中的重点和难点。由于数字可能包含多位,我们需要维护一个临时缓冲区,当遇到数字字符时持续读取,直到遇到非数字字符时才将缓冲区内容转换为数值。对于支持小数的情况,还需要额外处理小数点。下面是一个基础的词法分析函数实现,它能够正确解析整数和浮点数:
// 词法分析:从指定位置开始解析一个数字
double parseNumber(const std::string& expr, size_t& pos) {
size_t start = pos;
while (pos < expr.length() && (isdigit(expr[pos]) || expr[pos] == '.')) {
pos++;
}
return std::stod(expr.substr(start, pos - start));
}
// 判断字符是否为运算符
bool isOperator(char ch) {
return ch == '+' || ch == '-' || ch == '*' || ch == '/';
}
// 获取运算符优先级
int getPriority(char op) {
if (op == '+' || op == '-') return 1;
if (op == '*' || op == '/') return 2;
return 0;
}
除了数字和运算符,括号也是表达式中的重要组成部分。左括号和右括号在词法分析阶段需要单独处理,它们不参与优先级比较,而是作为表达式嵌套的边界标记。当遇到左括号时直接压入操作符栈,遇到右括号时则不断弹出操作符并执行运算,直到遇到左括号为止。这种处理方式能够支持任意层级的括号嵌套,使计算器具备处理复杂表达式的能力。
三、双栈结构实现运算优先级
双栈结构是计算器程序的核心算法,它巧妙地利用栈的后进先出特性来处理运算符优先级问题。整个算法的思路是:遍历表达式中的每个词法单元,如果是数字则压入操作数栈,如果是运算符则先与操作符栈顶元素比较优先级。如果当前运算符优先级小于等于栈顶运算符,则先弹出栈顶运算符并从操作数栈弹出两个操作数执行运算,将结果重新压入操作数栈,然后再继续比较新的栈顶运算符。
这个比较和弹出的过程需要循环执行,直到当前运算符优先级大于栈顶运算符优先级或者操作符栈为空时,才将当前运算符压入操作符栈。这种机制确保了高优先级的运算符总是先执行,比如乘法会先于加法执行。当表达式遍历完成后,还需要将操作符栈中剩余的运算符依次弹出并执行运算,最终操作数栈中剩下的唯一元素就是表达式的计算结果。
下面是双栈算法的核心实现代码,展示了如何通过两个栈协同工作来完成表达式求值:
#include <iostream>
#include <stack>
#include <string>
#include <cctype>
#include <stdexcept>
class Calculator {
private:
std::stack<double> numStack; // 操作数栈
std::stack<char> opStack; // 操作符栈
int getPriority(char op) {
if (op == '+' || op == '-') return 1;
if (op == '*' || op == '/') return 2;
return 0;
}
void executeOp() {
double right = numStack.top(); numStack.pop();
double left = numStack.top(); numStack.pop();
char op = opStack.top(); opStack.pop();
double result = 0;
switch (op) {
case '+': result = left + right; break;
case '-': result = left - right; break;
case '*': result = left * right; break;
case '/':
if (right == 0) throw std::runtime_error("除数不能为零");
result = left / right;
break;
}
numStack.push(result);
}
public:
double calculate(const std::string& expr) {
size_t i = 0;
while (i < expr.length()) {
if (isspace(expr[i])) {
i++;
continue;
}
if (isdigit(expr[i]) || expr[i] == '.') {
size_t start = i;
while (i < expr.length() && (isdigit(expr[i]) || expr[i] == '.')) {
i++;
}
double num = std::stod(expr.substr(start, i - start));
numStack.push(num);
} else if (expr[i] == '(') {
opStack.push(expr[i]);
i++;
} else if (expr[i] == ')') {
while (!opStack.empty() && opStack.top() != '(') {
executeOp();
}
if (!opStack.empty()) opStack.pop();
i++;
} else if (expr[i] == '+' || expr[i] == '-' ||
expr[i] == '*' || expr[i] == '/') {
while (!opStack.empty() &&
getPriority(opStack.top()) >= getPriority(expr[i])) {
executeOp();
}
opStack.push(expr[i]);
i++;
} else {
throw std::runtime_error("非法字符");
}
}
while (!opStack.empty()) {
executeOp();
}
return numStack.top();
}
};
上述代码中,executeOp函数负责从两个栈中弹出操作数和操作符并执行运算,结果压回操作数栈。calculate函数是主入口,它遍历表达式字符串,根据字符类型分别处理数字、括号和运算符。对于右括号,会不断执行运算直到遇到左括号;对于运算符,会先比较优先级再决定是否立即执行运算。这种设计使得计算器能够正确处理任意复杂的四则运算表达式。
四、完整程序测试与功能扩展
完成了核心计算逻辑后,我们需要编写主函数来测试计算器的功能。测试时应该覆盖各种典型场景,包括简单的加减乘除、带括号的嵌套表达式、连续运算、小数运算等。同时还需要测试异常情况,比如除数为零、括号不匹配、非法字符等,确保程序能够给出合理的错误提示而不是直接崩溃。
int main() {
Calculator calc;
std::string expressions[] = {
"3 + 5 * 2", // 13
"(3 + 5) * 2", // 16
"10 / 2 - 3", // 2
"2.5 * 4 + 1.5", // 11.5
"((1 + 2) * (3 + 4))" // 21
};
for (const auto& expr : expressions) {
try {
double result = calc.calculate(expr);
std::cout << expr << " = " << result << std::endl;
} catch (const std::exception& e) {
std::cerr << "计算错误: " << e.what() << std::endl;
}
}
return 0;
}
在实际应用中,计算器程序还可以进行多方面的功能扩展。比如支持更多数学运算符如幂运算、取模运算;支持数学函数如sin、cos、sqrt等;支持变量存储和引用;支持表达式预处理如去除空格、处理负数等。这些扩展都可以在现有双栈架构基础上进行,只需要在词法分析阶段增加相应的识别逻辑,在运算执行阶段增加对应的处理分支即可。
对于负数的处理是一个需要特别注意的扩展点。在标准四则运算中,负号和减号使用同一个符号,需要根据上下文判断其含义。如果减号前面是数字或右括号,则它是运算符;如果前面是左括号或表达式开头,则它是负号。实现时可以在词法分析阶段增加判断逻辑,当遇到减号时检查前一个字符的类型,从而决定将其作为负号处理还是作为减法运算符处理。
通过以上设计和实现,我们构建了一个功能完整的C++计算器程序。这个程序采用双栈算法处理运算符优先级,支持括号嵌套和浮点数运算,具备良好的错误处理能力。理解这个程序的实现原理,不仅有助于掌握栈数据结构的应用,也为后续学习编译原理中的表达式解析打下了坚实基础。