名人說:路漫漫其修遠兮,吾將上下而求索。—— 屈原《離騷》
創作者: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😊)
·