天津网站建设哪家公司好,wordpress站点是什么,wordpress与数据库,wordpress 跳转 微信支付文章目录 1.预备知识1.简单计算器的实现2.快捷键1.全局快捷键1.创建2.注销 2.局部快捷键 3.计算机内部浮点数的存储1.浮点数表示格式2.浮点数的计算3.实例 2.实验目的3.实验内容4.代码实现1.界面编写2.准备工作3.数字输入4.特殊数字输入#xff08;指数与小数#xff09;1.指… 文章目录 1.预备知识1.简单计算器的实现2.快捷键1.全局快捷键1.创建2.注销 2.局部快捷键 3.计算机内部浮点数的存储1.浮点数表示格式2.浮点数的计算3.实例 2.实验目的3.实验内容4.代码实现1.界面编写2.准备工作3.数字输入4.特殊数字输入指数与小数1.指数2.小数 5.退格键处理6.清空操作7.运算符输入1.左括号2.右括号3.加号4.减号5.乘号6.除号7.乘幂8.开方9.取余 8.计算结果1.准备2.数字操作处理3.数字输入状态改变4.运算符操作1.数字处理与入栈2.计算指定范围的表达式3.查找待计算范围4.移除指定范围的栈顶元素5.打包计算6.合并数字处理与运算符入栈 5.括号操作6.最终程序 9.快捷键设置 5.运行结果6.总结1.实验中遇到的困难如何处理退格键如何对浮点数进行取余如何处理运算符入栈 2.心得体会 1.预备知识
1.简单计算器的实现
已知有一个简单字符表达式包含四则运算、括号、取余、开方以及乘幂要求计算该表达式的运算结果。
实现简易计算器的主要数据结构为栈另外也可以使用字符串替换的方式实现详见C实现简单计算器(字符串替换)。由于字符串替换实现效率较低因此在本实验中我们使用栈的方式实现。
实现简单计算器的一般步骤可概括如下
操作数栈 创建一个用于存储操作数的栈。这是一个后进先出的数据结构对于计算器而言我们可以使用栈来跟踪操作数的顺序。每当我们在表达式中遇到一个数字我们将其推入操作数栈。运算符栈 创建另一个栈用于存储运算符。这个栈在处理表达式时将会派上用场。运算符栈用于管理运算符的优先级和结合性。中缀表达式转后缀表达式 这是一个关键步骤也被称为逆波兰表示法。我们遍历中缀表达式的每个元素将操作数直接添加到操作数栈中。对于运算符我们需要考虑它们的优先级和结合性。根据这些规则我们将运算符加入或弹出运算符栈。这个过程确保了我们按照正确的顺序执行操作。 操作数直接入栈 当遇到数字时我们将其推入操作数栈。运算符处理 对于每个运算符我们检查其与运算符栈顶元素的优先级。如果当前运算符的优先级较高或者与栈顶元素的优先级相等但是是右结合性的运算符那么我们将当前运算符推入运算符栈。否则我们将运算符栈顶元素弹出并加入到操作数栈中直到满足前述条件。 后缀表达式计算 有了后缀表达式我们可以遍历它并进行计算。当我们遇到一个数字时将其推入操作数栈。当遇到一个运算符时从操作数栈中弹出相应数量的操作数进行运算然后将结果再次推入操作数栈。这样我们逐步计算整个表达式直到得到最终结果。特殊运算符处理 如果计算器支持其他特殊运算符比如开方、乘幂等需要在计算过程中进行额外的处理。例如对于开方运算符我们可以使用数学库中的开方函数进行计算。错误处理 考虑在代码中加入错误处理机制以处理不合法的表达式、除零错误等异常情况。
由于在本实验中我们可以获取用户输入字符的顺序因此我们可以进一步简化程序将特殊运算符和四则运算进行统一处理。此外通过这个顺序我们还可以省略后缀表达式转换步骤直接对操作数栈的栈顶元素与当前数据进行计算。具体来说在扫描用户操作遇到一个运算符op不是括号与开方时如果栈为空直接将其进栈如果栈非空只有当op的优先级高于栈顶运算符的优先级时才直接将op进栈以后op先出栈表示先执行它否则依次出栈运算符与操作数与当前数据进行计算直到栈顶运算符的优先级小于等于op的优先级为止然后再将op进栈。
此外值得注意的是-既可以表示减号也可以表示对某个数据进行取负因此需要单独讨论。
2.快捷键
1.全局快捷键
1.创建
函数原型如下
BOOL WINAPI RegisterHotKey(
__in_opt HWND hWnd,
__in int id,
__in UINT fsModifiers,
__in UINT vk
);参数说明
hWnd 接收热键产生WM_HOTKEY消息的窗口句柄。若该参数NULL传递给调用线程的WM_HOTKEY消息必须在消息循环中进行处理。id 定义热键的标识符。调用线程中的其他热键不能使用同样的标识符。应用程序必须定义一个0X0000-0xBFFF范围的值。一个共享的动态链接库必须定义一个范围为0xC000-0xFFFF的值GlobalAddAtomA函数返回该范围。为了避免与其他动态链接库定义的热键冲突一个动态链接库必须使用GlobalAddAtomA函数获得热键的标识符。fsModifoers 定义为了产生WM_HOTKEY消息而必须与由nVirtKey参数定义的键一起按下的键。vk 定义热键的虚拟键码。
其中nVirtKey参数可以是如下值的组合
键值含意MOD_ALT0x0001按下的可以是任一Alt键。MOD_SHIFT0x0004按下的可以是任一Shift键。MOD_WIN0x0008按下的可以是任一Windows徽标键。MOD_NOREPEAT0x4000更改热键行为以便键盘自动重复不会产生多个热键通知。MOD_CONTROL0x0002按下的可以是任一Ctrl键。
若函数调用成功返回一个非0值。若函数调用失败则返回值为0。若要获得更多的错误信息可以调用GetLastError函数。
注意事项
当某键被接下时系统在所有的热键中寻找匹配者。一旦找到一个匹配的热键系统将把WM_HOTKEY消息传递给登记了该热键的线程的消息队列。该消息被传送到队列头部因此它将在下一轮消息循环中被移去。该函数不能将热键同其他线程创建的窗口关联起来。若为一热键定义的击键己被其他热键所定义则RegisterHotKey函数调用失败。若hWnd参数标识的窗口已用与id参数定义的相同的标识符登记了一个热键则参数fsModifiers和vk的新值将替代这些参数先前定义的值。Windows CE 2.0以上版本对于参数fsModifiers支持一个附加的标志位。叫做MOD_KEYUP。若设置MOD_KEYUP位则当发生键被按下或被弹起的事件时窗口将发送WM_HOTKEY消息。RegisterHotKey可以被用来在线程之间登记热键。
2.注销
函数原型如下
BOOL WINAPI UnRegisterHotKey(
_in_opt HWND hWnd,
_in int id
);参数说明
hWnd 与被释放的热键相关的窗口句柄。若热键不与窗口相关则该参数为NULL。id 定义被释放的热键的标识符。
若函数调用成功返回值不为0。若函数调用失败返回值为0。若要获得更多的错误信息可以调用GetLastError函数。
2.局部快捷键
在资源视图中新建Accelerator。进入资源后依次输入控件ID以及对应的快捷键。在对话框头文件中添加变量
HACCEL m_hAccelTable;在对话框初始化函数中添加初始化语句
m_hAccelTable LoadAccelerators(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_ACCELERATOR1));重写消息筛选函数PreTranslateMessage添加语句
if (::TranslateAccelerator(m_hWnd, m_hAccelTable, pMsg))return TRUE;3.计算机内部浮点数的存储
计算机内部存储浮点数采用IEEE 754标准。IEEE 754是一种二进制浮点数算术标准它定义了浮点数的表示、运算规则以及异常处理方式。这个标准广泛应用于计算机硬件和软件确保了浮点数在不同系统上的一致性。
IEEE 754标准定义了两种浮点数表示格式单精度和双精度。单精度使用32位4字节存储而双精度使用64位8字节存储。由于单精度的精度往往不满足实际需求本实验中我采用了双精度浮点数进行计算下面以双精度为例。
1.浮点数表示格式
符号位1位 决定浮点数的正负。0表示正数1表示负数。指数部分11位 表示浮点数的阶码exponent。这个阶码用来表示数的次方。具体的阶码值是无符号整数但实际上采用偏移值表示其中中间值是 2 ( k − 1 ) − 1 2^{(k-1)} - 1 2(k−1)−1其中k是指数部分的位数。这种表示方式有助于处理负指数。尾数部分52位 也称为尾数mantissa或小数部分用来表示浮点数的小数部分。尾数是一个二进制小数范围在1.0到2.0之间因此不需要存储小数点前的1。
2.浮点数的计算
浮点数的加法、减法、乘法和除法等操作都是按照IEEE 754标准定义的规则进行的。这些规则包括舍入方式、溢出处理和对特殊值如无穷大和NaN的处理。
3.实例
以双精度浮点数为例一个浮点数的表示如下
s | exp | mantissa
------------------------------------------------
0 | 01111111111 | 0100000000000000000000000000000000000000000000000000其中s是符号位exp是指数部分mantissa是尾数部分。这个例子表示的是 ( − 1 ) 0 ⋅ 2 ( 2047 − 1023 ) ⋅ 1.0 (-1)^0\cdot2^{(2047-1023)}\cdot1.0 (−1)0⋅2(2047−1023)⋅1.0因为指数部分采用偏移表示实际的指数是2047-10231024。
这种浮点数表示方式允许计算机表示广泛的数值范围并且提供了一定的精度但也存在由于浮点数运算导致的舍入误差和精度丢失的问题需要在程序设计中谨慎处理。
2.实验目的
掌握MFC的常用控件。
3.实验内容
计算器设计一个下图所示计算器。包含的功能有加、减、乘、除运算开方、倒数、求余等功能。输入的原始数据、运算中间数据和结果都显示在窗口顶部的同一个标签中。 其中“0”不能做除数。“Backspace”按钮可以清除上一次输入的数据“Clear”按钮可以清除所有已输入的数据。
4.代码实现
首先利用向导创建项目选择“基于对话框”后完成。
1.界面编写
打开资源视图根据实验题目修改对应各控件的Caption属性使用对齐工具进行布局另外修改静态文本控件的Client Edge属性为True。
2.准备工作
首先为静态文本控件添加变量CString showStr用于显示结果。
由于需要用栈可以使用std::stack但std::stack不方便遍历因此使用std::vector进行用户输入顺序的存储同时也充当栈的作用。此外由于需要计算开方与乘幂需要包含cmath数学库。
#include vector
#include stdint.h
#include cmath
using std::vector;然后如果直接使用、-……的字面字符量对用户数据进行储存将使得运算符的优先级判断大大复杂化因此引入如下宏
// 定义用户操作
#define OP_SQRT \0
#define OP_LEFT_BRACKET \1
#define OP_RIGHT_BRACKET \2
#define OP_PLUS \3
#define OP_MINUS \4
#define OP_MUL \5
#define OP_DIV \6
#define OP_MOD \7
#define OP_POW \10
#define NUM_0 \11
#define NUM_1 \12
#define NUM_2 \13
#define NUM_3 \14
#define NUM_4 \15
#define NUM_5 \16
#define NUM_6 \17
#define NUM_7 \20
#define NUM_8 \21
#define NUM_9 \22
#define NUM_DOT \23
#define NUM_E \24
#define OP_CAL \25最后定义一个全局容器用于存储用户操作
vectoruint8_t op; // 用户操作3.数字输入
由于对数字消息的处理都很类似因此首先定义处理宏。首先判断当前状态是否为已计算的状态
#define JUDGE_CALCATED \if (!op.empty() op.back() OP_CAL) \{ \op.clear(); \showStr TEXT(); \}然后处理数字输入事件
#define NUM_CLICKED(x) \JUDGE_CALCATED \op.push_back(NUM_##x); \showStr TEXT(#x); \UpdateData(FALSE);最后只需在对应的按钮事件处理函数中添加对应的处理宏即可这里不再一一列出。
4.特殊数字输入指数与小数
1.指数
编程发现指数的消息处理完全与普通数字相同因此直接使用数字输入处理宏
// x10^*
void CEx6Dlg::OnBnClickedButton13()
{NUM_CLICKED(E)
}2.小数
由于小数点不能出现在宏定义中因此手动将处理宏展开编写如下
// .
void CEx6Dlg::OnBnClickedButton14()
{JUDGE_CALCATEDop.push_back(NUM_DOT);showStr TEXT(.);UpdateData(FALSE);
}5.退格键处理
除了开方5个字符和取余3个字符以外其它操作的显示都是1个字符相应处理showStr并删除容器对应操作即可。
// backspace
void CEx6Dlg::OnBnClickedButton2()
{if(op.empty())return;switch (op.back()){case OP_MOD:showStr.Delete(showStr.GetLength() - 3, 3);break;case OP_SQRT:showStr.Delete(showStr.GetLength() - 5, 5);break;default:showStr.Delete(showStr.GetLength() - 1, 1);}op.pop_back();UpdateData(FALSE);
}6.清空操作
清空容器与字符串即可
// clear
void CEx6Dlg::OnBnClickedButton1()
{op.clear();showStr TEXT();UpdateData(FALSE);
}7.运算符输入
由于运算符数量不多而且不方便统一处理因此逐个编写即可。
1.左括号
// (
void CEx6Dlg::OnBnClickedButton23()
{JUDGE_CALCATEDop.push_back(OP_LEFT_BRACKET);showStr TEXT(();UpdateData(FALSE);
}2.右括号
// )
void CEx6Dlg::OnBnClickedButton24()
{JUDGE_CALCATEDop.push_back(OP_RIGHT_BRACKET);showStr TEXT());UpdateData(FALSE);
}3.加号
//
void CEx6Dlg::OnBnClickedButton15()
{JUDGE_CALCATEDop.push_back(OP_PLUS);showStr TEXT();UpdateData(FALSE);
}4.减号
// -
void CEx6Dlg::OnBnClickedButton16()
{JUDGE_CALCATEDop.push_back(OP_MINUS);showStr TEXT(-);UpdateData(FALSE);
}5.乘号
// *
void CEx6Dlg::OnBnClickedButton17()
{JUDGE_CALCATEDop.push_back(OP_MUL);showStr TEXT(*);UpdateData(FALSE);
}6.除号
// /
void CEx6Dlg::OnBnClickedButton18()
{JUDGE_CALCATEDop.push_back(OP_DIV);showStr TEXT(/);UpdateData(FALSE);
}7.乘幂
// ^
void CEx6Dlg::OnBnClickedButton19()
{JUDGE_CALCATEDop.push_back(OP_POW);showStr TEXT(^);UpdateData(FALSE);
}8.开方
// sqrt
void CEx6Dlg::OnBnClickedButton20()
{JUDGE_CALCATEDop.push_back(OP_SQRT);showStr TEXT(sqrt();UpdateData(FALSE);
}9.取余
// mod
void CEx6Dlg::OnBnClickedButton21()
{JUDGE_CALCATEDop.push_back(OP_MOD);showStr TEXT(mod);UpdateData(FALSE);
}8.计算结果
计算表达式的值需要遍历全局的用户操作容器操作又可以大致分为数字类、数字输入状态类、运算符类和括号类下面依次实现。
1.准备
首先如果表达式已计算直接返回即可
if (op.back() OP_CAL)return;接着定义当前数据、运算符栈与操作数栈、循环所需的迭代器以及当前数字输入状态指示器
double x(0.), e, d; // x:当前数字 e:指数 d:小数位
vectordouble numStack;
vectoruint8_t opStack;
vectoruint8_t::iterator i(op.begin()), opP1, opP2, opP3;
vectordouble::iterator numP1, numP2, numP3;
uint8_t numState(\0); // 数字输入状态: 第1位:是否在输入小数 第2位:是否在输入指数
// 第3位:是否为负数 第4位:是否为负指数2.数字操作处理
需要根据数字输入状态指示器选择正确的当前数字操作
#define CAL_CASE_NUM(curNum) \if (numState \2) \if (numState \1) \if (numState \10) \e - curNum * (d / 10); \else \e curNum * (d / 10); \else if (numState \10) \(e * 10) - curNum; \else \(e * 10) curNum; \else if (numState \1) \if (numState \4) \x - curNum * (d / 10); \else \x curNum * (d / 10); \else if (numState \4) \(x * 10) - curNum; \else \(x * 10) curNum; \break;3.数字输入状态改变
分为小数改变与指数改变。注意到指数位上也可以有小数输入因此小数状态改变只需要改变小数指示位用按位或即可。而指数输入相当于重新开始输入了一个新数因此直接赋值即可。
4.运算符操作
在正式计算之前容易知道开方运算符是个单目运算符并且优先级最高因此可将它暂时视为左括号处理我们将会在括号部分单独处理它。有了以上准备我们可以实现操作符入栈的过程。其核心思想就是计算栈顶所有优先级高于当前运算符的运算符需要注意运算符的结合性。
1.数字处理与入栈
首先容易知道当用户输入了一个运算符这表明数字输入的结束我们需要将指数合并到当前数字x中如果有的话
// 合并指数
#define CAL_NUM_WITH_EXP \if (numState \2) \x * pow(10., e);输入完一个数据以后不仅需要合并指数还需要重置数字输入状态指示器
// 合并数据并重置状态
#define SET_NUM_STATE \CAL_NUM_WITH_EXP \numState \0;对于一些异常情况我们可能需要终止运算并“抛出”异常[^1]在抛出之前我们需要添加已计算标记
#define ADD_CAL_SIGN \if (op.back() ! OP_CAL) \op.push_back(OP_CAL);最后实现异常的“抛出”
#define THROW_ERROR(condition, err_msg) \if (condition) \{ \MessageBox(TEXT(err_msg)); \ADD_CAL_SIGN \return; \}2.计算指定范围的表达式
由于没有进行后缀表达式的转换过程因此需要将不同优先级的运算符分别讨论。
首先是计算范围[p1,p2)内的所有乘幂
#define CAL_RANGE_POW(p1, p2) \while (p1 ! p2) \{ \THROW_ERROR(isnan(*(p1 1) pow(*p1, *(p1 1))), 乘幂计算有误!) \p1; \}下面计算乘除、取余和加减的表达式在这之前我们首先实现根据运算符求值的宏
#define __CAL_MDM(num1, num2, op_ID) \switch (op_ID) \{ \case OP_MUL: \num2 * num1; \break; \case OP_DIV: \THROW_ERROR(!(num2), 除数为零!) \num2 num1 / num2; \break; \case OP_MOD: \THROW_ERROR(!(num2), 除数为零!) \num2 fmod(num1, num2); \}加减的可类似实现
#define __CAL_PM(num1, num2, op_ID) \if (op_ID OP_PLUS) \num2 num1; \else \num2 num1 - num2;由于运算符不单一在计算范围乘除和加减时还需要传入运算符指针1
#define CAL_RANGE_MDM(np1, np2, op) \while (np1 ! np2) \{ \__CAL_MDM(*np1, *(np1 1), *op) \np1; \}
#define CAL_RANGE_PM(np1, np2, op) \while (np1 ! np2) \{ \__CAL_PM(*np1, *(np1 1), *op) \np1; \}3.查找待计算范围
由于同一优先级的运算是自左向右进行的我们需要从栈顶开始逐个向下查找直到找到目标优先级的运算符为止。
首先编写一个通用的查找模板2
#define FIND_RANGE(np1, np2, op, top, condition, opLowerBound) \top op; \while (op ! opLowerBound) \if (condition) \{ \op; \break; \} \np1 np2 - (top - op);然后依次模板“实例化”即可
#define FIND_RANGE_POW(np1, np2, op, top, opLowerBound) FIND_RANGE(np1, np2, op, top, *--op ! OP_POW, opLowerBound)
#define FIND_RANGE_MDM(np1, np2, op, top, opLowerBound) FIND_RANGE(np1, np2, op, top, *--op ! OP_MUL *op ! OP_DIV *op ! OP_MOD, opLowerBound)
#define FIND_RANGE_PM(np1, np2, op, top, opLowerBound) FIND_RANGE(np1, np2, op, top, *--op ! OP_PLUS *op ! OP_MINUS, opLowerBound)4.移除指定范围的栈顶元素
结束计算时需要移除栈顶元素直接调用erase方法即可
#define CAL_REMOVE(np, op) \if (op ! opStack.end()) \opStack.erase(op, opStack.end()); \if (np ! numStack.end()) \numStack.erase(np, numStack.end());5.打包计算
有了以上准备我们可以实现已知优先级的计算处理宏。又由于需计算乘除的一定需计算乘幂需计算加减的一定需计算乘除。所以分别进行宏合并即可。3
首先是只计算乘幂适用于乘、除、取余入栈
#define _CAL_POW(np1, np2, op, top, tnp, opLowerBound) \np2 numStack.end() - 1; \op opStack.end(); \FIND_RANGE_POW(np1, np2, op, top, opLowerBound) \top op; \tnp np1; \CAL_RANGE_POW(np1, np2) \*tnp *np2;接着是计算乘幂、乘除和取余适用于加减入栈
#define _CAL_MDM(np1, np2, op, top, tnp, opLowerBound) \_CAL_POW(np1, np2, op, top, tnp, opLowerBound) \op top; \np2 tnp; \FIND_RANGE_MDM(np1, np2, op, top, opLowerBound) \tnp np1; \top op; \CAL_RANGE_MDM(np1, np2, op) \*tnp *np2;然后是计算全部运算符适用于括号与结束入栈
#define _CAL_PM(np1, np2, op, top, tnp, opLowerBound) \_CAL_MDM(np1, np2, op, top, tnp, opLowerBound) \op top; \np2 tnp; \FIND_RANGE_PM(np1, np2, op, top, opLowerBound) \tnp np1; \top op; \CAL_RANGE_PM(np1, np2, op) \*tnp *np2;最后加上出栈代码实现完整计算过程
#define CAL_POW(np1, np2, op, top, tnp, opLowerBound) \_CAL_POW(np1, np2, op, top, tnp, opLowerBound) \tnp; \CAL_REMOVE(tnp, top)
#define CAL_MDM(np1, np2, op, top, tnp, opLowerBound) \_CAL_MDM(np1, np2, op, top, tnp, opLowerBound) \tnp; \CAL_REMOVE(tnp, top)
#define CAL_PM(np1, np2, op, top, tnp, opLowerBound) \_CAL_PM(np1, np2, op, top, tnp, opLowerBound) \tnp; \CAL_REMOVE(tnp, top)6.合并数字处理与运算符入栈
由于乘幂不需要进行计算处理完数字直接入栈即可处理宏如下
#define CASE_OP(ID) \SET_NUM_STATE \numStack.push_back(x); \x 0.; \opStack.push_back(ID); \break;此外将实际定义的迭代器传入宏作参数并计算入栈编写处理宏如下
#define CASE_POW CAL_POW(numP1, numP2, opP1, opP2, numP3, opStack.begin())
#define CASE_MDM CAL_MDM(numP1, numP2, opP1, opP2, numP3, opStack.begin())
#define CASE_PM CAL_PM(numP1, numP2, opP1, opP2, numP3, opStack.begin())
#define CASE_OPC(ID, case) \SET_NUM_STATE \numStack.push_back(x); \if (!numStack.empty()) \{CASE_##case} x 0.; \opStack.push_back(ID); \break;5.括号操作
对于左括号和根号直接入栈即可。对于右括号只需调用计算加减的情况即可。此外还需要判断是否需要将结果开根以及是否有左括号与其匹配的问题。
6.最终程序
//
void CEx6Dlg::OnBnClickedButton22()
{if (op.back() OP_CAL)return;double x(0.), e, d; // x:当前数字 e:指数 d:小数位vectordouble numStack;vectoruint8_t opStack;vectoruint8_t::iterator i(op.begin()), opP1, opP2, opP3;vectordouble::iterator numP1, numP2, numP3;uint8_t numState(\0); // 数字输入状态: 第1位:是否在输入小数 第2位:是否在输入指数// 第3位:是否为负数 第4位:是否为负指数// IndexStack bracketPosStack;// CBitStack bracketStack; // true表示开方, 否则为左括号doswitch (*i){case NUM_0:CAL_CASE_NUM(0)case NUM_1:CAL_CASE_NUM(1)case NUM_2:CAL_CASE_NUM(2)case NUM_3:CAL_CASE_NUM(3)case NUM_4:CAL_CASE_NUM(4)case NUM_5:CAL_CASE_NUM(5)case NUM_6:CAL_CASE_NUM(6)case NUM_7:CAL_CASE_NUM(7)case NUM_8:CAL_CASE_NUM(8)case NUM_9:CAL_CASE_NUM(9)case NUM_DOT:numState | \1;d 1.;break;case NUM_E:numState \2;e 0.;break;case OP_PLUS:CASE_OPC(OP_PLUS, MDM)case OP_MINUS:if (i op.begin() || *(i - 1) OP_SQRT || *(i - 1) OP_LEFT_BRACKET){numState | \4;break;}if (*(i - 1) NUM_E){numState | \10;break;}CASE_OPC(OP_MINUS, MDM)case OP_MUL:CASE_OPC(OP_MUL, POW)case OP_DIV:CASE_OPC(OP_DIV, POW)case OP_POW:CASE_OP(OP_POW)case OP_MOD:CASE_OPC(OP_MOD, POW)case OP_SQRT:opStack.push_back(OP_SQRT);break;case OP_LEFT_BRACKET:opStack.push_back(OP_LEFT_BRACKET);break;case OP_RIGHT_BRACKET:SET_NUM_STATEnumStack.push_back(x);CASE_PMTHROW_ERROR(opStack.empty() || opStack.back() ! OP_SQRT opStack.back() ! OP_LEFT_BRACKET, 括号不匹配!)if (opStack.back() OP_SQRT){THROW_ERROR(numStack.back() 0, 负数不能开方!)numStack.back() sqrt(numStack.back());}opStack.pop_back();x numStack.back();numStack.pop_back();}while (i ! op.end());CAL_NUM_WITH_EXPnumStack.push_back(x);CASE_PMCString s;s.Format(TEXT(%f), numStack.back());showStr s;UpdateData(FALSE);ADD_CAL_SIGN
}9.快捷键设置
在资源视图中新建Accelerator并依次添加控件如下 然后在头文件中添加变量
HACCEL m_hAccelTable;接着重写对话框初始化函数
BOOL CEx6Dlg::OnInitDialog()
{CDialogEx::OnInitDialog();// 将“关于...”菜单项添加到系统菜单中。// IDM_ABOUTBOX 必须在系统命令范围内。ASSERT((IDM_ABOUTBOX 0xFFF0) IDM_ABOUTBOX);ASSERT(IDM_ABOUTBOX 0xF000);CMenu* pSysMenu GetSystemMenu(FALSE);if (pSysMenu ! NULL){BOOL bNameValid;CString strAboutMenu;bNameValid strAboutMenu.LoadString(IDS_ABOUTBOX);ASSERT(bNameValid);if (!strAboutMenu.IsEmpty()){pSysMenu-AppendMenu(MF_SEPARATOR);pSysMenu-AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);}}// 设置此对话框的图标。 当应用程序主窗口不是对话框时框架将自动// 执行此操作SetIcon(m_hIcon, TRUE); // 设置大图标SetIcon(m_hIcon, FALSE); // 设置小图标// TODO: 在此添加额外的初始化代码m_hAccelTable LoadAccelerators(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_ACCELERATOR1));return TRUE; // 除非将焦点设置到控件否则返回 TRUE
}最后重写消息筛选函数
BOOL CEx6Dlg::PreTranslateMessage(MSG* pMsg)
{if (::TranslateAccelerator(m_hWnd, m_hAccelTable, pMsg))return TRUE;return CDialogEx::PreTranslateMessage(pMsg);
}5.运行结果
一个包含所有运算符的表达式 若分母为零 若括号不匹配 若对负数开方 若乘幂结果为复值
6.总结
1.实验中遇到的困难
如何处理退格键
一开始的思路是将数字栈和运算符栈设置在全局区一边输入一边进栈处理。但是这样必然导致需要一个历史记录容器进行存储历史操作反而使问题复杂化了因此将数字栈和运算符栈设置为局部变量。
如何对浮点数进行取余
一开始通过学习计算机组成原理实现了浮点数判断整数的函数但是将浮点数强转为整型时存在溢出问题。4
最终了解到了C语言的数学库中的fmod函数解决了问题。如下为浮点数判断整数的实现
inline bool IsNotInt(const double x) noexcept
{const long long t(*(const long long *)(const void *)x);int E(((t 52) 0x7ff) - 1023);return !(E 0x80000000) (E 52 || (t 0x000fffffffffffff) (E 12));
}此外还类似MATLAB中的fix实现了截断浮点数
inline double fix(const double x) noexcept
{const unsigned long long t(*(const unsigned long long *)(const void *)x);short e((2047 int(t 52)) - 1023);if (e 0)return 0.;if (e 51)return x;return *(double *)(void *)(t (~((1 52 - e) - 1)));
}如何处理运算符入栈
原本使用std::stack直接依次出栈但这样改变了运算符的运算顺序导致了诸如3-2-12等荒谬错误正确做法是查找后逐个自左向右计算。
2.心得体会
在本次实验中我深刻体会到了计算器的基本原理和计算表达式的复杂性。在实现计算器的过程中我选择了直接对中缀表达式进行计算而不是先将中缀表达式转换成后缀表达式。这使得代码相对简洁但在处理运算符优先级和括号时增加了一些复杂性。处理运算符的优先级和结合性是计算器实现中的重要挑战之一。在我的实现中通过使用不同的处理宏和迭代器我成功地处理了不同运算符的优先级和结合性。在实现中我加入了一些简单的错误处理机制例如除数为零、括号不匹配等情况。这是为了使计算器更加健壮和用户友好。通过设置快捷键和加速键我为用户提供了更便捷的操作方式。这是一个提高用户体验的重要方面。在处理浮点数取余时我发现C语言的数学库提供了方便的函数fmod避免了一些复杂的实现。
由于时间原因这个计算器做的很粗糙没对许多错误表达式进行判断。比如当用户输入连续的小数点或运算符时需要添加相应的异常处理若后续有空余时间可以继续完善相应的异常处理部分增加程序的健壮性。
总的来说通过本次实验我对计算器的实现有了更深刻的理解同时也学到了如何处理运算符、优先级和错误情况。这对于理解编程中的算法逻辑和错误处理机制都有很大的帮助。
代码地址https://github.com/zsc118/MFC-exercises 这里实际上是迭代器其作用和指针类似姑且算作指针。 ↩︎ 这里考虑到后面的括号可能需要不同的查找下界尽管最后没用上这个opLowerBound参数还是保存了下来。 ↩︎ 这里实际上用不到加减的计算但由于后面的括号和结束运算时的需要这里同一处理了加减。 ↩︎ 哪怕是long long也会溢出。 ↩︎