C++多功能计算器:从表达式求值到工程实践
1. 项目概述
“多功能计算器”这个项目标题,听起来简单,但背后能玩出的花样和学到的知识,远超一个简单的“加减乘除”程序。它几乎是每个C++初学者都会接触的经典练手项目,也是检验你是否真正理解面向对象编程、数据结构、算法乃至软件工程思想的绝佳试金石。我见过太多人止步于一个简陋的命令行计算器,但一个真正“多功能”的计算器,应该能处理复杂的表达式、支持括号优先级、具备历史记录、甚至提供图形界面。这个项目不仅能帮你巩固C++基础语法,更能让你深入理解从需求分析、架构设计到代码实现、调试优化的完整开发流程。无论你是刚学完C++基础语法的学生,还是想通过一个完整项目来串联知识点的开发者,这个项目都能让你获益匪浅。接下来,我将以一个从业者的视角,带你从零开始,构建一个不仅功能强大、而且代码健壮、易于扩展的C++多功能计算器。
2. 项目整体设计与思路拆解
2.1 核心需求与功能定义
一个“多功能”计算器,其核心远不止四则运算。我们需要先明确它的能力边界。基于常见的用户场景和教学目的,我将其核心功能划分为几个层次:
- 基础运算层:支持加(+)、减(-)、乘(*)、除(/)、求模(%)等基本算术运算。这是基石。
- 表达式处理层:这是核心难点。用户希望输入像
(3 + 4) * 2 / (10 - 5)这样的复杂表达式,而不是一次只能算两个数。这要求程序能理解运算符优先级和括号。 - 高级功能层:包括科学计算函数(如sin, cos, sqrt, pow)、常量(如π、e)、历史记录查看与回滚、错误处理(如除零、表达式不合法)等。
- 交互界面层:可以是命令行界面(CLI),也可以是图形用户界面(GUI)。CLI适合学习核心逻辑,GUI则能提供更好的用户体验。
对于本项目,我们将聚焦于实现一个支持复杂表达式、具备基本错误处理、拥有历史记录功能的命令行计算器。这是一个在技术深度和实用性上取得良好平衡的选择。
2.2 技术选型与架构设计
为什么选择C++?因为它的性能、对内存的精细控制以及面向对象特性,非常适合用来实现这种需要高效解析和计算的核心逻辑模块。我们的架构将采用经典的分层设计:
- 表示层(Presentation Layer):负责与用户交互,接收输入字符串,格式化输出结果。在我们的CLI版本中,就是
main函数中的输入输出循环。 - 业务逻辑层(Business Logic Layer):这是核心。我们将创建一个
Calculator核心类。它不关心输入从哪里来,只负责接收一个代表数学表达式的字符串,并返回计算结果。这个类内部会封装表达式解析和计算的所有复杂逻辑。 - 数据访问层(Data Access Layer):负责持久化数据,例如计算历史记录。我们可以用一个简单的
HistoryManager类来管理,将记录存储在内存向量中,也可以扩展为写入文件。
关键的技术点在于表达式求值。我们有两种主流方案:
- 中缀表达式直接求值:边解析边计算,需要处理运算符栈和操作数栈,逻辑相对复杂。
- 中缀转后缀(逆波兰表达式),再求值:这是更清晰、更模块化的方法。先将人类习惯的“中缀表达式”转换为计算机更容易处理的“后缀表达式”,然后再对后缀表达式求值。这种方法将复杂的优先级和括号处理问题,分解为两个相对独立的步骤,代码结构更优美,也更容易调试。
我强烈推荐方案二。它不仅是一个经典的算法问题(面试常考),而且其模块化的思想对培养良好的编程习惯至关重要。我们的Calculator类内部就可以划分为Parser(解析器/转换器)和Evaluator(求值器)两个主要组件。
2.3 开发环境与工具链
工欲善其事,必先利其器。一个顺手的开发环境能极大提升效率。
- 集成开发环境(IDE):Visual Studio(Windows)或CLion(跨平台)是首选。它们提供了强大的代码补全、调试器和项目管理功能。对于初学者,Visual Studio的“编辑并继续”功能在调试时非常有用。
- 编译器:使用IDE通常会自带编译器(如MSVC或MinGW)。确保你的项目配置正确。
- 版本控制:从一开始就使用Git。无论是用VS内置的Git工具还是命令行,定期提交代码是一个必须养成的好习惯。这不仅是备份,更是你开发思路的轨迹记录。
- 构建系统:对于简单项目,可以直接使用IDE的项目文件(
.sln,.vcxproj)。如果追求可移植性和现代性,可以学习使用CMake。它允许你用一套脚本在不同平台上生成构建文件。
实操心得:在项目根目录初始化Git仓库后,第一件事就是创建
.gitignore文件,忽略掉构建目录(如build/、Debug/、Release/)、IDE配置文件等。这能保持仓库的整洁。
3. 核心模块实现详解
3.1 表达式求值引擎:中缀转后缀算法
这是整个项目的“心脏”。我们来深入拆解“中缀转后缀”算法。
算法核心思想:利用一个栈来临时存放运算符。从左到右扫描中缀表达式:
- 遇到操作数(数字),直接输出到后缀表达式。
- 遇到运算符,将其与栈顶运算符比较优先级:
- 若栈空,或栈顶是左括号
(,或当前运算符优先级高于栈顶运算符,则当前运算符入栈。 - 否则,将栈顶运算符弹出并输出,然后重复步骤2的比较。
- 若栈空,或栈顶是左括号
- 遇到左括号
(,直接入栈。 - 遇到右括号
),则不断弹出栈顶运算符并输出,直到遇到左括号(,然后丢弃这对括号。 - 表达式扫描完毕后,将栈中所有剩余运算符依次弹出并输出。
优先级定义:通常乘除(*,/,%)优先级高于加减(+,-)。我们可以用一个std::map<char, int>来映射。
代码实现要点:
- 数字识别:需要处理多位数和小数点。不能简单地按字符分割。我们可以用一个循环,直到遇到非数字或小数点为止,将这段字符拼接成一个完整的数字字符串。
- 负号处理:这是易错点。表达式开头的负号(如
-5+3)或括号后的负号(如(-5+3))是一元运算符,而减号是二元运算符。需要在解析时根据上下文进行区分。一个简单的策略是:如果当前字符是-,且它前面是操作符或表达式开头,则将其视为负号,我们可以将其转换为(0-数字)的形式,或者特殊标记。 - 错误处理:在转换过程中,就要检查括号是否匹配、表达式是否合法(例如两个运算符连续出现)。
// 示例:中缀表达式转后缀表达式的核心函数片段 std::vector<std::string> InfixToPostfix(const std::string& infix) { std::vector<std::string> postfix; // 存储后缀表达式(每个元素是操作数或运算符) std::stack<char> opStack; std::map<char, int> precedence = { {'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}, {'%', 2} }; size_t i = 0; while (i < infix.length()) { // 跳过空格 if (isspace(infix[i])) { ++i; continue; } // 处理数字(包括小数) if (isdigit(infix[i]) || infix[i] == '.') { std::string num; while (i < infix.length() && (isdigit(infix[i]) || infix[i] == '.')) { num += infix[i++]; } postfix.push_back(num); continue; } // 处理左括号 if (infix[i] == '(') { opStack.push(infix[i++]); continue; } // 处理右括号 if (infix[i] == ')') { while (!opStack.empty() && opStack.top() != '(') { postfix.push_back(std::string(1, opStack.top())); opStack.pop(); } if (opStack.empty()) { throw std::runtime_error("Mismatched parentheses!"); } opStack.pop(); // 弹出左括号 ++i; continue; } // 处理运算符 if (precedence.find(infix[i]) != precedence.end()) { // 处理可能的负号(一元运算符) if (infix[i] == '-' && (i == 0 || infix[i-1] == '(' || precedence.find(infix[i-1]) != precedence.end())) { // 这是一个负号,不是减号。一种处理方式:将“-数字”整体作为一个操作数 // 这里我们采用另一种策略:在后续求值时特殊处理。先按运算符入栈,但做标记。 // 为简化,我们可以在数字识别阶段处理,这里先按普通运算符处理。 } while (!opStack.empty() && opStack.top() != '(' && precedence[opStack.top()] >= precedence[infix[i]]) { postfix.push_back(std::string(1, opStack.top())); opStack.pop(); } opStack.push(infix[i++]); continue; } // 如果遇到无法识别的字符,抛出异常 throw std::runtime_error("Invalid character in expression: " + std::string(1, infix[i])); } // 将栈中剩余运算符弹出 while (!opStack.empty()) { if (opStack.top() == '(') { throw std::runtime_error("Mismatched parentheses!"); } postfix.push_back(std::string(1, opStack.top())); opStack.pop(); } return postfix; }3.2 后缀表达式求值
得到后缀表达式后,求值就简单多了。同样使用一个栈,这次是操作数栈(std::stack<double>)。
算法步骤:
- 从左到右扫描后缀表达式。
- 遇到操作数,将其转换为
double后压入操作数栈。 - 遇到运算符,从栈中弹出两个操作数(注意顺序:先弹出的是右操作数,后弹出的是左操作数),执行相应运算,将结果压回栈中。
- 扫描结束后,栈中应只剩下一个元素,即为最终结果。
代码实现要点:
- 除零错误:在执行除法前必须检查除数是否为零。
- 操作数不足:如果遇到运算符时栈中元素少于2个,说明表达式不合法。
- 最终状态:求值完成后,栈中必须恰好有一个元素,否则表达式不合法。
// 示例:后缀表达式求值 double EvaluatePostfix(const std::vector<std::string>& postfix) { std::stack<double> valStack; for (const auto& token : postfix) { // 如果是操作数 if (isdigit(token[0]) || (token[0] == '-' && token.length() > 1)) { // 简单判断,实际需更严谨 valStack.push(std::stod(token)); } else { // 是运算符 if (valStack.size() < 2) { throw std::runtime_error("Invalid expression: insufficient operands for operator " + token); } double right = valStack.top(); valStack.pop(); double left = valStack.top(); valStack.pop(); double result = 0.0; switch (token[0]) { case '+': result = left + right; break; case '-': result = left - right; break; case '*': result = left * right; break; case '/': if (std::abs(right) < 1e-12) { // 处理浮点数除零 throw std::runtime_error("Math error: Division by zero!"); } result = left / right; break; case '%': // 注意:% 运算符通常用于整数,这里需要类型转换和判断 if (std::abs(right) < 1e-12) { throw std::runtime_error("Math error: Modulo by zero!"); } result = std::fmod(left, right); // 使用fmod处理浮点数取模 break; default: throw std::runtime_error("Unsupported operator: " + token); } valStack.push(result); } } if (valStack.size() != 1) { throw std::runtime_error("Invalid expression!"); } return valStack.top(); }3.3 Calculator核心类的封装
现在,我们将解析和求值逻辑封装进一个Calculator类。这个类对外提供一个简洁的接口:Calculate(const std::string& expression)。
类设计:
- 私有成员函数:
InfixToPostfix: 实现上述转换算法。EvaluatePostfix: 实现上述求值算法。IsOperator,GetPrecedence: 辅助函数。
- 公有成员函数:
Calculate: 主接口,接收中缀表达式字符串,返回计算结果。内部调用私有函数。GetLastResult/GetHistory: 可选,用于获取上一次结果或历史记录。
头文件 (Calculator.h) 示例:
// Calculator.h #pragma once #include <string> #include <vector> #include <stack> class Calculator { public: // 计算表达式的主函数 double Calculate(const std::string& expression); // 获取上一次的计算结果(可选) double GetLastResult() const; // 获取计算历史(可选) const std::vector<std::pair<std::string, double>>& GetHistory() const; // 清空历史(可选) void ClearHistory(); private: // 内部辅助函数和状态 std::vector<std::string> InfixToPostfix(const std::string& infix); double EvaluatePostfix(const std::vector<std::string>& postfix); bool IsOperator(char c) const; int GetPrecedence(char op) const; double lastResult_ = 0.0; std::vector<std::pair<std::string, double>> history_; // 存储表达式和结果对 };注意事项:类的设计要遵循“单一职责原则”。
Calculator类只负责计算核心逻辑。输入输出、历史记录管理(如果复杂)可以考虑拆分成单独的类。
3.4 用户交互与主循环设计
有了强大的计算引擎,我们需要一个友好的界面来驱动它。在CLI中,主循环 (main函数) 负责:
- 打印欢迎信息和用法说明。
- 进入一个
while循环,持续提示用户输入。 - 读取用户输入的一行字符串。
- 调用
Calculator::Calculate进行计算。 - 捕获可能抛出的异常(如除零错误、表达式错误),并给出友好的错误提示。
- 打印结果,并询问是否继续或退出。
- 实现一些内建命令,如输入
history查看历史,输入clear清屏,输入exit或quit退出程序。
// main.cpp 主循环核心片段 #include "Calculator.h" #include <iostream> #include <string> int main() { Calculator calc; std::string input; std::cout << "=== C++多功能计算器 ===" << std::endl; std::cout << "支持运算符: +, -, *, /, %, ( )" << std::endl; std::cout << "输入 'history' 查看记录,'clear' 清屏,'exit' 退出" << std::endl; std::cout << "请输入表达式 (例如: (3+4)*2 ):" << std::endl; while (true) { std::cout << ">>> "; if (!std::getline(std::cin, input)) { break; // 处理EOF (Ctrl+Z on Windows, Ctrl+D on Linux) } // 处理内建命令 if (input == "exit" || input == "quit") { std::cout << "再见!" << std::endl; break; } if (input == "history") { auto history = calc.GetHistory(); if (history.empty()) { std::cout << "历史记录为空。" << std::endl; } else { for (const auto& entry : history) { std::cout << entry.first << " = " << entry.second << std::endl; } } continue; } if (input == "clear") { // 跨平台清屏(简易版) #ifdef _WIN32 system("cls"); #else system("clear"); #endif continue; } if (input.empty()) { continue; } // 核心计算逻辑 try { double result = calc.Calculate(input); std::cout << "结果: " << result << std::endl; } catch (const std::exception& e) { std::cout << "错误: " << e.what() << std::endl; } } return 0; }4. 功能扩展与高级特性实现
一个基础的计算器完成后,我们可以为其添加更多“多功能”特性,使其更实用、更健壮。
4.1 科学计算函数支持
要支持sin,cos,sqrt,pow,log等函数,我们需要扩展表达式解析器。
实现思路:
- 词法分析升级:在
InfixToPostfix函数中,不能只把连续的字母当作错误。当扫描到字母时,需要尝试读取一个完整的函数名(如sin,cos)。 - 函数作为特殊操作符:将函数名视为一种特殊的、高优先级的“运算符”。它需要一个操作数(对于单参数函数)或两个操作数(如
pow)。 - 后缀表达式中的表示:在后缀表达式中,函数名可以作为一个特殊的 token,比如
"sin","sqrt"。 - 求值器升级:在
EvaluatePostfix中,当遇到函数 token 时,从栈中弹出相应数量的操作数,调用对应的数学函数(std::sin,std::sqrt等),再将结果压栈。
关键修改点:
- 在
InfixToPostfix中增加对字母的识别,并维护一个已知函数名的集合。 - 函数的优先级通常是最高的,并且是右结合的(对于嵌套函数)。
- 需要处理函数参数周围的括号,例如
sin(30)。
4.2 变量与常量支持
允许用户定义变量(如x = 10)并在后续表达式中使用(如x * 2),或者使用预定义常量(如PI,E)。
实现思路:
- 符号表:使用一个
std::map<std::string, double>来存储变量名和其对应的值。 - 赋值语句解析:在主循环中,判断输入是否包含
=。如果有,则将其解析为赋值语句,将等号右边的表达式计算结果存入符号表。 - 表达式中的变量替换:在
InfixToPostfix或求值之前,需要将表达式中的变量名替换为其对应的数值。这可以在词法分析阶段完成:当识别出一个标识符(由字母开头)时,去符号表中查找其值,并将其替换为数字 token。 - 常量:预定义的常量(如
PI)可以初始化时就放入符号表中。
4.3 健壮的错误处理与输入验证
一个专业的程序必须能优雅地处理各种错误输入,而不是崩溃。
- 异常处理:如我们之前所做,在
Calculate函数内部用try-catch块包裹核心逻辑,将解析或计算中的错误转换为std::runtime_error异常抛出,在主循环中捕获并显示。 - 输入验证:
- 检查括号是否匹配。
- 检查运算符位置是否合法(不能连续出现两个运算符,除非是负号)。
- 检查函数名是否合法。
- 检查变量名是否合法(只包含字母数字和下划线,不以数字开头)。
- 浮点数精度问题:直接比较
double是否等于零可能有问题。应使用一个极小的误差范围(如1e-12)来判断,如上文除零检查所示。
4.4 历史记录功能的实现
历史记录功能不仅能提升用户体验,也是学习数据持久化的好例子。
内存中历史记录:
- 在
Calculator类中添加一个std::vector<std::pair<std::string, double>> history_成员。 - 每次成功计算后,将表达式和结果组成的对(pair)存入向量。
- 提供
GetHistory()方法返回这个向量的常量引用。 - 提供
ClearHistory()方法清空向量。
文件持久化:
- 可以增加
SaveHistoryToFile(const std::string& filename)和LoadHistoryFromFile(const std::string& filename)方法。 - 使用
std::ofstream和std::ifstream进行文件读写。 - 文件格式可以很简单,每行存储“表达式=结果”。
- 注意:文件操作可能失败(如权限不足、磁盘满),需要进行错误处理。
5. 项目构建、调试与测试
5.1 使用CMake管理项目(进阶)
对于希望项目更规范、跨平台的开发者,强烈推荐使用CMake。它比直接使用IDE项目文件更灵活。
一个简单的CMakeLists.txt示例:
cmake_minimum_required(VERSION 3.10) project(MultiFunctionCalculator) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 将源代码文件添加到可执行目标 add_executable(Calculator src/main.cpp src/Calculator.cpp ) # 如果Calculator类使用了数学库,需要链接它 target_link_libraries(Calculator PRIVATE m) # 在Linux/macOS上链接数学库 # 包含头文件目录 target_include_directories(Calculator PRIVATE include)将头文件放在include/目录,源文件放在src/目录,然后在项目根目录执行:
mkdir build && cd build cmake .. cmake --build .即可生成可执行文件。
5.2 系统化的调试技巧
调试是开发中不可或缺的一环。除了设置断点、单步执行(F10/F11)、查看变量等基本操作,还有一些针对本项目的特定技巧:
- 表达式解析调试:在
InfixToPostfix函数的关键节点(如遇到操作数、运算符、括号时)打印日志,输出当前栈的状态和已生成的后缀表达式。这能帮你直观地理解算法流程。 - 后缀表达式输出:在
Calculate函数中,先将转换得到的后缀表达式打印出来。手动验证这个后缀表达式是否正确,可以快速定位是解析问题还是求值问题。 - 条件断点:例如,只在除数为零时触发断点。在VS中,右键点击断点 -> “条件”,输入
right == 0(或abs(right) < 1e-12)。 - “编辑并继续”:在Visual Studio中,如果你在调试时修改了代码(在不改变函数签名等重大结构的情况下),可以继续执行而无需重新编译,极大提升调试效率。
5.3 单元测试的引入
为了保证代码质量,特别是核心算法模块的可靠性,应该编写单元测试。
- 测试框架:可以使用 Google Test、Catch2 等轻量级C++测试框架。
- 测试什么:
InfixToPostfix函数:给定中缀表达式,检查输出的后缀表达式是否正确。EvaluatePostfix函数:给定后缀表达式,检查计算结果是否正确。Calculate函数:集成测试,输入完整表达式,检查最终结果。- 边界情况:空输入、单个数字、嵌套括号、连续运算符、除零、非法字符等。
- 示例(使用Catch2):
运行测试可以确保你的修改不会破坏已有功能。#define CATCH_CONFIG_MAIN #include <catch2/catch.hpp> #include "Calculator.h" TEST_CASE("Basic arithmetic", "[calculator]") { Calculator calc; REQUIRE(calc.Calculate("2+3") == Approx(5)); REQUIRE(calc.Calculate("10-4") == Approx(6)); REQUIRE(calc.Calculate("6*7") == Approx(42)); REQUIRE(calc.Calculate("15/3") == Approx(5)); } TEST_CASE("Operator precedence", "[calculator]") { Calculator calc; REQUIRE(calc.Calculate("2+3*4") == Approx(14)); // 乘法优先 REQUIRE(calc.Calculate("(2+3)*4") == Approx(20)); // 括号改变优先级 } TEST_CASE("Division by zero throws", "[calculator]") { Calculator calc; REQUIRE_THROWS_AS(calc.Calculate("5/0"), std::runtime_error); }
6. 常见问题与排查技巧实录
在实际开发中,你一定会遇到各种“坑”。以下是我总结的一些典型问题及其解决方案。
6.1 浮点数精度导致的“不相等”
问题:测试0.1 + 0.2 == 0.3时返回false。原因:二进制浮点数无法精确表示某些十进制小数(如0.1),存在微小的舍入误差。解决:永远不要直接用==比较浮点数。应使用一个允许的误差范围(epsilon)。
bool IsEqual(double a, double b, double epsilon = 1e-12) { return std::abs(a - b) < epsilon; } // 在测试和比较中使用 IsEqual(result, expected)6.2 栈操作顺序错误
问题:计算5 - 3得到-2而不是2。原因:在后缀表达式求值时,从栈中弹出操作数的顺序错了。对于减法a - b,后缀表达式是a b -。求值时,先弹出的是b(右操作数),后弹出的是a(左操作数)。如果顺序反了,就会得到b - a。解决:牢记“后进先出”原则。对于二元运算符,先弹出的是右操作数。在代码中明确注释:
double right = valStack.top(); valStack.pop(); // 先弹出的是右操作数 double left = valStack.top(); valStack.pop(); // 后弹出的是左操作数 double result = left - right; // 左操作数 运算符 右操作数6.3 内存泄漏与资源管理
问题:虽然本项目主要使用栈(stack)对象,但如果在扩展时使用了new分配内存,忘记delete会导致内存泄漏。解决:遵循RAII(资源获取即初始化)原则。
- 优先使用标准库容器(
std::vector,std::string,std::stack),它们会自动管理内存。 - 如果必须使用动态内存,使用智能指针(
std::unique_ptr,std::shared_ptr),避免手动new/delete。 - 在类的析构函数中确保释放所有资源。
6.4 多字节字符与中文输入
问题:在控制台输入中文后,程序可能解析混乱或崩溃。原因:C++默认的char和std::string处理的是单字节字符。中文字符在UTF-8编码下占多个字节。解决:对于计算器,我们通常只处理ASCII数学符号和数字。可以在读取输入后,过滤掉非ASCII字符,或者明确提示用户只输入英文和数字符号。如果需要国际化和本地化,则需要使用std::wstring和宽字符函数,但这会大大增加复杂度,对于本练习项目不建议。
6.5 表达式解析中的负号处理
问题:表达式-5+3或3*-2解析错误。原因:词法分析器将-统一识别为减号(二元运算符),但在这些位置它应该被识别为负号(一元运算符)。解决:在InfixToPostfix函数中,当遇到-时,需要根据上下文判断:
- 如果它在表达式开头,或前一个字符是
(或另一个运算符,那么它是一元负号。 - 对于一元负号,一种处理策略是将其转换为
(0 - ...)的形式。例如,将-5在解析前替换为(0-5)。另一种策略是在后缀表达式中引入一个特殊的一元运算符(如~),并在求值器里特殊处理。
一个相对简单的实现是:在扫描表达式时,如果遇到-且满足一元条件,我们将其替换为一个特殊的标记(如#表示一元负),然后在求值阶段,当遇到#时,只弹出一个操作数进行取反操作。
6.6 性能优化考量
对于计算器这种规模的项目,性能通常不是瓶颈。但养成良好的习惯很重要:
- 避免不必要的拷贝:函数参数尽量使用
const std::string&传递字符串。 - 使用
reserve:如果事先知道std::vector或std::string的大致大小,使用reserve()预分配内存,可以减少多次重新分配的开销。 switch优于if-else链:在运算符分发的场景(如EvaluatePostfix中的switch),switch语句通常比一长串if-else更高效,编译器可能生成跳转表。
7. 从命令行到图形界面(Qt入门)
如果你想让计算器拥有一个真正的窗口界面,Qt是一个绝佳的选择。它是一个成熟的跨平台C++ GUI框架。
迁移思路:
- 核心逻辑复用:我们之前精心编写的
Calculator类完全不需要修改!这就是分层架构的好处。GUI层只负责界面交互,计算逻辑完全由底层的Calculator类负责。 - 创建Qt项目:使用Qt Creator新建一个Qt Widgets Application项目。
- 设计界面:使用Qt Designer拖拽按钮(0-9,运算符,括号,等号)、一个行编辑框(QLineEdit)用于显示表达式和结果、一个文本浏览器(QTextBrowser)用于显示历史记录。
- 连接信号与槽:将按钮的
clicked()信号连接到对应的槽函数。例如,数字按钮将对应的字符追加到行编辑框;等号按钮则获取行编辑框的文本,调用Calculator::Calculate,并将结果显示在行编辑框,同时更新历史记录区域。 - 处理错误:在调用
Calculate时使用try-catch,如果捕获到异常,用一个消息框(QMessageBox)提示用户。
一个简单的等号按钮槽函数示例:
void MainWindow::on_equalsButton_clicked() { QString expression = ui->lineEdit->text(); if (expression.isEmpty()) return; try { double result = calculator_.Calculate(expression.toStdString()); ui->lineEdit->setText(QString::number(result, 'g', 15)); // 显示结果 // 更新历史记录 QString historyEntry = QString("%1 = %2").arg(expression).arg(result); ui->historyBrowser->append(historyEntry); } catch (const std::exception& e) { QMessageBox::critical(this, "计算错误", e.what()); } }通过这种方式,你就能将一个强大的计算引擎与一个美观的图形界面结合起来,完成一个完整的桌面应用程序。这不仅是功能的提升,更是对你软件架构能力的一次极好锻炼。
