C++课设:简易科学计算器(支持+-*/、sin、cos、tan、log等科学函数)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》
创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊)
专栏介绍:《编程项目实战》
目录
- 一、项目概览与设计理念
- 1. 功能特色
- 2. 技术架构
- 二、核心算法深度解析
- 1. 词法分析(Tokenization)
- 2. 中缀转后缀(调度场算法)
- 3. 后缀表达式求值
- 三、代码架构与类设计
- 1. Calculator类的整体结构
- 2. 函数注册机制
- 四、特色功能详解
- 1. 角度与弧度的智能处理
- 2. 错误处理与用户体验
- 3. 交互式演示功能
- 五、实际运行演示
- 1. 基础运算示例
- 2. 科学计算示例
- 3. 复杂表达式示例
- 六、完整源代码
- 七、总结与扩展思考
在编程学习的道路上,表达式求值一直是一个经典而重要的问题。今天我们将深入探讨如何用C++从零开始构建一个功能完整的科学计算器,它不仅支持基础的四则运算,还能处理复杂的科学函数计算。这个项目将带你领略词法分析、调度场算法、后缀表达式求值等核心计算机科学概念的魅力。
一、项目概览与设计理念
1. 功能特色
这个计算器具备以下核心功能:
- 基础运算:加减乘除、幂运算(支持
^
和**
两种语法) - 科学函数:三角函数、对数函数、开方、绝对值等
- 角度支持:同时支持弧度和角度计算(
sin
vssind
) - 智能解析:完整的表达式词法分析和语法解析
- 错误处理:友好的错误提示和异常处理
2. 技术架构
整个计算器采用三阶段处理流程:
输入表达式 → 词法分析 → 中缀转后缀 → 后缀求值 → 输出结果
这种设计遵循了编译原理的经典思想,将复杂问题分解为独立的处理阶段,每个阶段都有明确的职责。
二、核心算法深度解析
1. 词法分析(Tokenization)
词法分析是整个计算过程的第一步,它将输入的字符串分解为有意义的词法单元(Token)。
enum TokenType {NUMBER, // 数字OPERATOR, // 操作符FUNCTION, // 函数LEFT_PAREN, // 左括号RIGHT_PAREN // 右括号
};struct Token {TokenType type;string value;double numValue;
};
词法分析器会识别以下模式:
- 数字识别:支持整数和浮点数
- 操作符识别:包括特殊的双字符操作符
**
- 函数识别:动态匹配预定义的函数名
- 括号匹配:正确处理嵌套括号
2. 中缀转后缀(调度场算法)
这里使用了著名的调度场算法(Shunting-yard Algorithm),这是计算机科学家Edsger Dijkstra发明的经典算法。
为什么要转换为后缀表达式?
- 消除了操作符优先级的歧义
- 无需括号就能明确表达计算顺序
- 求值过程更加简单高效
vector<Token> infixToPostfix(const vector<Token>& tokens) {vector<Token> output;stack<Token> operators;for (const Token& token : tokens) {switch (token.type) {case NUMBER:output.push_back(token);break;case OPERATOR:// 处理操作符优先级和结合性while (!operators.empty() && shouldPopOperator(token, operators.top())) {output.push_back(operators.top());operators.pop();}operators.push(token);break;// ... 其他情况处理}}return output;
}
3. 后缀表达式求值
后缀表达式的求值过程非常直观:遇到数字压栈,遇到操作符弹出操作数计算。
double evaluatePostfix(const vector<Token>& postfix) {stack<double> values;for (const Token& token : postfix) {if (token.type == NUMBER) {values.push(token.numValue);} else if (token.type == OPERATOR) {double b = values.top(); values.pop();double a = values.top(); values.pop();values.push(calculate(a, token.value, b));}// ... 函数处理}return values.top();
}
三、代码架构与类设计
1. Calculator类的整体结构
class Calculator {
private:map<string, int> operatorPrecedence; // 操作符优先级map<string, bool> rightAssociative; // 右结合性标记map<string, double (*)(double)> functions; // 函数指针映射public:Calculator() {initOperators();initFunctions();}double calculate(const string& expression);void showInfixToPostfix(const string& expression);void showSupportedFunctions();
};
这种设计的优势:
- 封装性好:所有计算逻辑都在类内部
- 可扩展性强:新增函数只需修改初始化代码
- 配置化:操作符优先级和函数都通过映射表管理
2. 函数注册机制
计算器使用函数指针映射实现动态函数调用:
void initFunctions() {// 基础数学函数functions["sin"] = sin;functions["cos"] = cos;functions["sqrt"] = sqrt;// 角度版本的三角函数functions["sind"] = sind;functions["cosd"] = cosd;functions["tand"] = tand;
}
这种设计使得添加新函数变得极其简单,只需要定义函数并注册到映射表中即可。
四、特色功能详解
1. 角度与弧度的智能处理
这个计算器的一大亮点是同时支持角度和弧度计算:
// 弧度版本(标准数学函数)
sin(1.57) // ≈ 1.0// 角度版本(添加d后缀)
sind(90) // = 1.0
实现原理:
static double sind(double deg) {return sin(degToRad(deg));
}static double degToRad(double deg) {return deg * 3.14159265358979323846 / 180.0;
}
2. 错误处理与用户体验
代码中实现了多层次的错误处理:
try {double result = calc.calculate(input);cout << "结果: " << fixed << setprecision(6) << result << endl;
} catch (const exception& e) {cout << "错误: " << e.what() << endl;
}
常见错误类型:
- 除零错误:
if (b == 0) throw runtime_error("除零错误");
- 语法错误:括号不匹配、操作符缺少操作数
- 函数错误:未知函数名、参数缺失
3. 交互式演示功能
计算器提供了丰富的演示功能:
// 输入 "demo" 查看处理过程
calc.showInfixToPostfix("3 + 4 * 2 / ( 1 - 5 ) ^ 2");// 输出:
// 原始表达式: 3 + 4 * 2 / ( 1 - 5 ) ^ 2
// 词法分析结果: 3 4 2 * 1 5 - 2 ^ / +
// 后缀表达式: 3 4 2 * 1 5 - 2 ^ / +
五、实际运行演示
1. 基础运算示例
请输入表达式: 3 + 4 * 2
结果: 11.000000请输入表达式: 2 ^ 3 * 4
结果: 32.000000请输入表达式: sqrt(16) + log10(100)
结果: 6.000000
2. 科学计算示例
请输入表达式: sind(30) + cosd(60)
结果: 1.000000请输入表达式: log(exp(5))
结果: 5.000000
3. 复杂表达式示例
请输入表达式: sin(deg2rad(45)) * sqrt(2)
结果: 1.000000请输入表达式: (sind(30) + cosd(60)) * log10(100)
结果: 2.000000
六、完整源代码
以下是完整的计算器实现代码:
#include <iostream>
#include <string>
#include <vector>
#include <stack>
#include <cmath>
#include <cctype>
#include <sstream>
#include <map>
#include <iomanip>
#include <stdexcept>
#include <cstdlib>using namespace std;// 枚举类型定义操作符和函数类型
enum TokenType {NUMBER,OPERATOR,FUNCTION,LEFT_PAREN,RIGHT_PAREN
};// 表示一个词法单元
struct Token {TokenType type;string value;double numValue;Token(TokenType t, const string& v, double n = 0) : type(t), value(v), numValue(n) {}
};class Calculator {
private:map<string, int> operatorPrecedence;map<string, bool> rightAssociative;map<string, double (*)(double)> functions;void initOperators() {// 设置操作符优先级operatorPrecedence["+"] = 1;operatorPrecedence["-"] = 1;operatorPrecedence["*"] = 2;operatorPrecedence["/"] = 2;operatorPrecedence["^"] = 3;operatorPrecedence["**"] = 3;// 设置右结合性(只有幂运算是右结合的)rightAssociative["^"] = true;rightAssociative["**"] = true;}// 角度转弧度辅助函数static double degToRad(double deg) {return deg * 3.14159265358979323846 / 180.0;}static double radToDeg(double rad) {return rad * 180.0 / 3.14159265358979323846;}// 角度版本的三角函数static double sind(double deg) {return sin(degToRad(deg));}static double cosd(double deg) {return cos(degToRad(deg));}static double tand(double deg) {return tan(degToRad(deg));}// 包装函数来避免直接使用库函数指针static double log_wrapper(double x) {return log(x);}static double log10_wrapper(double x) {return log10(x);}static double exp_wrapper(double x) {return exp(x);}static double sqrt_wrapper(double x) {return sqrt(x);}static double abs_wrapper(double x) {return fabs(x);}static double sin_wrapper(double x) {return sin(x);}static double cos_wrapper(double x) {return cos(x);}static double tan_wrapper(double x) {return tan(x);}static double ceil_wrapper(double x) {return ceil(x);}static double floor_wrapper(double x) {return floor(x);}void initFunctions() {// 注册科学计算函数(使用包装函数)functions["sin"] = sin_wrapper;functions["cos"] = cos_wrapper;functions["tan"] = tan_wrapper;// 注册角度版本的三角函数functions["sind"] = sind;functions["cosd"] = cosd;functions["tand"] = tand;// 其他数学函数(使用包装函数)functions["sqrt"] = sqrt_wrapper;functions["log"] = log_wrapper;functions["log10"] = log10_wrapper; // 重点修复functions["exp"] = exp_wrapper;functions["abs"] = abs_wrapper;functions["ceil"] = ceil_wrapper;functions["floor"] = floor_wrapper;// 角度转换函数functions["deg2rad"] = degToRad;functions["rad2deg"] = radToDeg;}// 词法分析器vector<Token> tokenize(const string& expression) {vector<Token> tokens;string current = "";for (size_t i = 0; i < expression.length(); i++) {char c = expression[i];if (isspace(c)) {continue;}if (isdigit(c) || c == '.') {current += c;} else {// 如果当前有数字,先处理数字if (!current.empty()) {double num = atof(current.c_str());tokens.push_back(Token(NUMBER, current, num));current = "";}// 处理操作符和特殊字符if (c == '+' || c == '-' || c == '*' || c == '/' || c == '^') {// 检查是否是幂运算符 **if (c == '*' && i + 1 < expression.length() && expression[i + 1] == '*') {tokens.push_back(Token(OPERATOR, "**"));i++; // 跳过下一个 *} else {string op(1, c);tokens.push_back(Token(OPERATOR, op));}} else if (c == '(') {tokens.push_back(Token(LEFT_PAREN, "("));} else if (c == ')') {tokens.push_back(Token(RIGHT_PAREN, ")"));} else if (isalpha(c)) {// 处理函数名(支持包含数字的函数名如log10)string funcName = "";while (i < expression.length() && (isalpha(expression[i]) || isdigit(expression[i]))) {funcName += expression[i];i++;}i--; // 回退一位,因为for循环会自增if (functions.find(funcName) != functions.end()) {tokens.push_back(Token(FUNCTION, funcName));} else {cout << "未知函数: " << funcName << endl;return vector<Token>(); // 返回空向量表示错误}} else {// 遇到不支持的字符,给出相应提示if (c < 0 || c > 127) {cout << "警告: 忽略特殊字符(可能是°符号)" << endl;cout << "提示: 请使用角度函数 sind(), cosd(), tand() 代替°符号" << endl;} else {cout << "警告: 忽略不支持的字符 '" << c << "'" << endl;}}}}// 处理最后的数字if (!current.empty()) {double num = atof(current.c_str());tokens.push_back(Token(NUMBER, current, num));}return tokens;}// 中缀转后缀(调度场算法)vector<Token> infixToPostfix(const vector<Token>& tokens) {vector<Token> output;stack<Token> operators;for (size_t i = 0; i < tokens.size(); i++) {const Token& token = tokens[i];switch (token.type) {case NUMBER:output.push_back(token);break;case FUNCTION:operators.push(token);break;case OPERATOR: {while (!operators.empty() && operators.top().type != LEFT_PAREN &&((operators.top().type == FUNCTION) ||(operators.top().type == OPERATOR &&((operatorPrecedence[operators.top().value] > operatorPrecedence[token.value]) ||(operatorPrecedence[operators.top().value] == operatorPrecedence[token.value] &&rightAssociative.find(token.value) == rightAssociative.end()))))) {output.push_back(operators.top());operators.pop();}operators.push(token);break;}case LEFT_PAREN:operators.push(token);break;case RIGHT_PAREN:while (!operators.empty() && operators.top().type != LEFT_PAREN) {output.push_back(operators.top());operators.pop();}if (!operators.empty() && operators.top().type == LEFT_PAREN) {operators.pop(); // 移除左括号}// 如果栈顶是函数,也要弹出if (!operators.empty() && operators.top().type == FUNCTION) {output.push_back(operators.top());operators.pop();}break;}}// 弹出剩余的操作符while (!operators.empty()) {output.push_back(operators.top());operators.pop();}return output;}// 计算后缀表达式double evaluatePostfix(const vector<Token>& postfix) {stack<double> values;for (size_t i = 0; i < postfix.size(); i++) {const Token& token = postfix[i];switch (token.type) {case NUMBER:values.push(token.numValue);break;case OPERATOR: {if (values.size() < 2) {throw runtime_error("表达式错误:操作符缺少操作数");}double b = values.top(); values.pop();double a = values.top(); values.pop();double result = 0;if (token.value == "+") {result = a + b;} else if (token.value == "-") {result = a - b;} else if (token.value == "*") {result = a * b;} else if (token.value == "/") {if (b == 0) {throw runtime_error("除零错误");}result = a / b;} else if (token.value == "^" || token.value == "**") {result = pow(a, b);}values.push(result);break;}case FUNCTION: {if (values.empty()) {throw runtime_error("表达式错误:函数缺少参数");}double arg = values.top(); values.pop();// 添加调试信息cout << "正在计算函数: " << token.value << "(" << arg << ")" << endl;if (functions.find(token.value) == functions.end()) {throw runtime_error("未知函数: " + token.value);}double result = functions[token.value](arg);cout << "结果: " << result << endl;values.push(result);break;}default:break;}}if (values.size() != 1) {throw runtime_error("表达式错误");}return values.top();}public:Calculator() {initOperators();initFunctions();}// 主要的计算函数double calculate(const string& expression) {// 1. 词法分析vector<Token> tokens = tokenize(expression);if (tokens.empty()) {throw runtime_error("无效的表达式");}// 调试:显示词法分析结果cout << "词法分析结果: ";for (size_t i = 0; i < tokens.size(); i++) {cout << "[" << tokens[i].value << "] ";}cout << endl;// 2. 中缀转后缀vector<Token> postfix = infixToPostfix(tokens);// 3. 计算后缀表达式return evaluatePostfix(postfix);}// 显示中缀转后缀的过程void showInfixToPostfix(const string& expression) {cout << "原始表达式: " << expression << endl;vector<Token> tokens = tokenize(expression);cout << "词法分析结果: ";for (size_t i = 0; i < tokens.size(); i++) {cout << tokens[i].value << " ";}cout << endl;vector<Token> postfix = infixToPostfix(tokens);cout << "后缀表达式: ";for (size_t i = 0; i < postfix.size(); i++) {cout << postfix[i].value << " ";}cout << endl;}// 显示支持的函数列表void showSupportedFunctions() {cout << "\n支持的科学计算函数:" << endl;cout << "=== 三角函数(弧度) ===" << endl;cout << "sin(x) - 正弦函数(x为弧度)" << endl;cout << "cos(x) - 余弦函数(x为弧度)" << endl;cout << "tan(x) - 正切函数(x为弧度)" << endl;cout << "\n=== 三角函数(角度) ===" << endl;cout << "sind(x) - 正弦函数(x为角度)" << endl;cout << "cosd(x) - 余弦函数(x为角度)" << endl;cout << "tand(x) - 正切函数(x为角度)" << endl;cout << "\n=== 其他数学函数 ===" << endl;cout << "sqrt(x) - 平方根" << endl;cout << "log(x) - 自然对数" << endl;cout << "log10(x) - 常用对数" << endl;cout << "exp(x) - 指数函数" << endl;cout << "abs(x) - 绝对值" << endl;cout << "ceil(x) - 向上取整" << endl;cout << "floor(x) - 向下取整" << endl;cout << "\n=== 角度转换函数 ===" << endl;cout << "deg2rad(x) - 角度转弧度" << endl;cout << "rad2deg(x) - 弧度转角度" << endl;cout << "\n支持的操作符: +, -, *, /, ^, ** (幂运算)" << endl;cout << "支持括号: ( )" << endl;cout << "\n使用示例:" << endl;cout << "sind(30) → 0.5 (30度的正弦值)" << endl;cout << "cosd(60) → 0.5 (60度的余弦值)" << endl;cout << "sin(deg2rad(30)) → 0.5 (先转弧度再计算)" << endl;}
};int main() {Calculator calc;string input;cout << "=== 简易计算器 (C++98兼容版) ===" << endl;cout << "输入 'help' 查看支持的函数" << endl;cout << "输入 'demo' 查看中缀转后缀演示" << endl;cout << "输入 'quit' 退出程序" << endl;cout << "\n重要提示:" << endl;cout << "? 角度计算请使用: sind(30), cosd(60), tand(45)" << endl;cout << "? 弧度计算请使用: sin(1.57), cos(3.14), tan(0.78)" << endl;cout << "? 不支持°符号,请直接输入数字" << endl;cout << "? 编译时请使用: g++ calculator.cpp -o calculator -lm" << endl;cout << "=================================" << endl;while (true) {cout << "\n请输入表达式: ";getline(cin, input);if (input == "quit" || input == "exit") {cout << "谢谢使用!" << endl;break;}if (input == "help") {calc.showSupportedFunctions();continue;}if (input == "demo") {cout << "\n=== 中缀转后缀演示 ===" << endl;calc.showInfixToPostfix("3 + 4 * 2 / ( 1 - 5 ) ^ 2");calc.showInfixToPostfix("sind(30) + cosd(60)");calc.showInfixToPostfix("sqrt(16) + log10(100)");cout << "\n=== 角度vs弧度计算演示 ===" << endl;cout << "sind(30) = " << calc.calculate("sind(30)") << " (30度的正弦值)" << endl;cout << "sin(30) = " << calc.calculate("sin(30)") << " (30弧度的正弦值)" << endl;cout << "cosd(60) = " << calc.calculate("cosd(60)") << " (60度的余弦值)" << endl;cout << "cos(60) = " << calc.calculate("cos(60)") << " (60弧度的余弦值)" << endl;cout << "sin(deg2rad(30)) = " << calc.calculate("sin(deg2rad(30))") << " (先转弧度)" << endl;continue;}if (input.empty()) {continue;}try {double result = calc.calculate(input);cout << "结果: " << fixed << setprecision(6) << result << endl;} catch (const exception& e) {cout << "错误: " << e.what() << endl;}}return 0;
}
七、总结与扩展思考
通过这个项目,我们深入学习了表达式求值的完整实现过程,掌握了从词法分析到语法解析再到求值计算的核心技术。这个计算器不仅是一个实用的工具,更是理解编译原理和算法设计的绝佳实践。
主要技术收获:
- 深入理解了调度场算法的工作原理
- 掌握了词法分析器的设计与实现
- 学会了使用函数指针映射实现动态调用
- 体验了面向对象设计的封装和扩展性
可能的扩展方向:
- 支持变量定义和表达式存储
- 添加矩阵运算和复数计算
- 实现图形化界面或Web版本
- 支持自定义函数定义和脚本执行
这个项目为我们打开了计算机语言处理的大门,无论是后续学习编译器设计,还是开发更复杂的数学计算工具,都有着重要的参考价值。
创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊)
·