文章目录
- 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_ALT | 0x0001 | 按下的可以是任一Alt 键。 |
MOD_SHIFT | 0x0004 | 按下的可以是任一Shift 键。 |
MOD_WIN | 0x0008 | 按下的可以是任一Windows 徽标键。 |
MOD_NOREPEAT | 0x4000 | 更改热键行为,以便键盘自动重复不会产生多个热键通知。 |
MOD_CONTROL | 0x0002 | 按下的可以是任一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-1023=1024。
这种浮点数表示方式允许计算机表示广泛的数值范围,并且提供了一定的精度,但也存在由于浮点数运算导致的舍入误差和精度丢失的问题,需要在程序设计中谨慎处理。
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'
最后,定义一个全局容器用于存储用户操作:
vector<uint8_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:小数位
vector<double> numStack;
vector<uint8_t> opStack;
vector<uint8_t>::iterator i(op.begin()), opP1, opP2, opP3;
vector<double>::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:小数位vector<double> numStack;vector<uint8_t> opStack;vector<uint8_t>::iterator i(op.begin()), opP1, opP2, opP3;vector<double>::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-1=2等荒谬错误,正确做法是查找后逐个自左向右计算。
2.心得体会
在本次实验中,我深刻体会到了计算器的基本原理和计算表达式的复杂性。在实现计算器的过程中,我选择了直接对中缀表达式进行计算,而不是先将中缀表达式转换成后缀表达式。这使得代码相对简洁,但在处理运算符优先级和括号时增加了一些复杂性。处理运算符的优先级和结合性是计算器实现中的重要挑战之一。在我的实现中,通过使用不同的处理宏和迭代器,我成功地处理了不同运算符的优先级和结合性。在实现中,我加入了一些简单的错误处理机制,例如除数为零、括号不匹配等情况。这是为了使计算器更加健壮和用户友好。通过设置快捷键和加速键,我为用户提供了更便捷的操作方式。这是一个提高用户体验的重要方面。在处理浮点数取余时,我发现C语言的数学库提供了方便的函数fmod
,避免了一些复杂的实现。
由于时间原因,这个计算器做的很粗糙,没对许多错误表达式进行判断。比如当用户输入连续的小数点或运算符时,需要添加相应的异常处理,若后续有空余时间,可以继续完善相应的异常处理部分,增加程序的健壮性。
总的来说,通过本次实验,我对计算器的实现有了更深刻的理解,同时也学到了如何处理运算符、优先级和错误情况。这对于理解编程中的算法逻辑和错误处理机制都有很大的帮助。
代码地址:https://github.com/zsc118/MFC-exercises
这里实际上是迭代器,其作用和指针类似,姑且算作指针。 ↩︎
这里考虑到后面的括号可能需要不同的查找下界,尽管最后没用上,这个
opLowerBound
参数还是保存了下来。 ↩︎这里实际上用不到加减的计算,但由于后面的括号和结束运算时的需要,这里同一处理了加减。 ↩︎
哪怕是
long long
也会溢出。 ↩︎