简易计算器的实现(MFC)

1.预备知识

1.简单计算器的实现

已知有一个简单字符表达式,包含四则运算、括号、取余、开方以及乘幂,要求计算该表达式的运算结果。

实现简易计算器的主要数据结构为栈,另外也可以使用字符串替换的方式实现,详见C++实现简单计算器(字符串替换)。由于字符串替换实现效率较低,因此在本实验中我们使用栈的方式实现。

实现简单计算器的一般步骤可概括如下:

  1. 操作数栈: 创建一个用于存储操作数的栈。这是一个后进先出的数据结构,对于计算器而言,我们可以使用栈来跟踪操作数的顺序。每当我们在表达式中遇到一个数字,我们将其推入操作数栈。
  2. 运算符栈: 创建另一个栈用于存储运算符。这个栈在处理表达式时将会派上用场。运算符栈用于管理运算符的优先级和结合性。
  3. 中缀表达式转后缀表达式: 这是一个关键步骤,也被称为"逆波兰表示法"。我们遍历中缀表达式的每个元素,将操作数直接添加到操作数栈中。对于运算符,我们需要考虑它们的优先级和结合性。根据这些规则,我们将运算符加入或弹出运算符栈。这个过程确保了我们按照正确的顺序执行操作。
    • 操作数直接入栈: 当遇到数字时,我们将其推入操作数栈。
    • 运算符处理: 对于每个运算符,我们检查其与运算符栈顶元素的优先级。如果当前运算符的优先级较高,或者与栈顶元素的优先级相等但是是右结合性的运算符,那么我们将当前运算符推入运算符栈。否则,我们将运算符栈顶元素弹出并加入到操作数栈中,直到满足前述条件。
  4. 后缀表达式计算: 有了后缀表达式,我们可以遍历它并进行计算。当我们遇到一个数字时,将其推入操作数栈。当遇到一个运算符时,从操作数栈中弹出相应数量的操作数进行运算,然后将结果再次推入操作数栈。这样,我们逐步计算整个表达式,直到得到最终结果。
  5. 特殊运算符处理: 如果计算器支持其他特殊运算符,比如开方、乘幂等,需要在计算过程中进行额外的处理。例如,对于开方运算符,我们可以使用数学库中的开方函数进行计算。
  6. 错误处理: 考虑在代码中加入错误处理机制,以处理不合法的表达式、除零错误等异常情况。

由于在本实验中,我们可以获取用户输入字符的顺序,因此我们可以进一步简化程序,将特殊运算符和四则运算进行统一处理。此外,通过这个顺序,我们还可以省略后缀表达式转换步骤,直接对操作数栈的栈顶元素与当前数据进行计算。具体来说,在扫描用户操作遇到一个运算符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
);

参数说明:

  1. hWnd 接收热键产生WM_HOTKEY消息的窗口句柄。若该参数NULL,传递给调用线程的WM_HOTKEY消息必须在消息循环中进行处理。
  2. id 定义热键的标识符。调用线程中的其他热键,不能使用同样的标识符。应用程序必须定义一个0X0000-0xBFFF范围的值。一个共享的动态链接库必须定义一个范围为0xC000-0xFFFF的值(GlobalAddAtomA函数返回该范围)。为了避免与其他动态链接库定义的热键冲突,一个动态链接库必须使用GlobalAddAtomA函数获得热键的标识符。
  3. fsModifoers 定义为了产生WM_HOTKEY消息而必须与由nVirtKey参数定义的键一起按下的键。
  4. vk 定义热键的虚拟键码。

其中,nVirtKey参数可以是如下值的组合:

含意
MOD_ALT0x0001按下的可以是任一Alt键。
MOD_SHIFT0x0004按下的可以是任一Shift键。
MOD_WIN0x0008按下的可以是任一Windows徽标键。
MOD_NOREPEAT0x4000更改热键行为,以便键盘自动重复不会产生多个热键通知。
MOD_CONTROL0x0002按下的可以是任一Ctrl键。

若函数调用成功,返回一个非0值。若函数调用失败,则返回值为0。若要获得更多的错误信息,可以调用GetLastError函数。

注意事项:

  • 当某键被接下时,系统在所有的热键中寻找匹配者。一旦找到一个匹配的热键,系统将把WM_HOTKEY消息传递给登记了该热键的线程的消息队列。该消息被传送到队列头部,因此它将在下一轮消息循环中被移去。该函数不能将热键同其他线程创建的窗口关联起来。
  • 若为一热键定义的击键己被其他热键所定义,则RegisterHotKey函数调用失败。
  • hWnd参数标识的窗口已用与id参数定义的相同的标识符登记了一个热键,则参数fsModifiersvk的新值将替代这些参数先前定义的值。
  • 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. 符号位(1位): 决定浮点数的正负。0表示正数,1表示负数。
  2. 指数部分(11位): 表示浮点数的阶码(exponent)。这个阶码用来表示数的次方。具体的阶码值是无符号整数,但实际上采用偏移值表示,其中中间值是 2 ( k − 1 ) − 1 2^{(k-1)} - 1 2(k1)1,其中k是指数部分的位数。这种表示方式有助于处理负指数。
  3. 尾数部分(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)02(20471023)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_CALCATED
	op.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_CALCATED
	op.push_back(OP_LEFT_BRACKET);
	showStr += TEXT("(");
	UpdateData(FALSE);
}

2.右括号

// )
void CEx6Dlg::OnBnClickedButton24()
{
	JUDGE_CALCATED
	op.push_back(OP_RIGHT_BRACKET);
	showStr += TEXT(")");
	UpdateData(FALSE);
}

3.加号

// +
void CEx6Dlg::OnBnClickedButton15()
{
	JUDGE_CALCATED
	op.push_back(OP_PLUS);
	showStr += TEXT("+");
	UpdateData(FALSE);
}

4.减号

// -
void CEx6Dlg::OnBnClickedButton16()
{
	JUDGE_CALCATED
	op.push_back(OP_MINUS);
	showStr += TEXT("-");
	UpdateData(FALSE);
}

5.乘号

// *
void CEx6Dlg::OnBnClickedButton17()
{
	JUDGE_CALCATED
	op.push_back(OP_MUL);
	showStr += TEXT("*");
	UpdateData(FALSE);
}

6.除号

// /
void CEx6Dlg::OnBnClickedButton18()
{
	JUDGE_CALCATED
	op.push_back(OP_DIV);
	showStr += TEXT("/");
	UpdateData(FALSE);
}

7.乘幂

// ^
void CEx6Dlg::OnBnClickedButton19()
{
	JUDGE_CALCATED
	op.push_back(OP_POW);
	showStr += TEXT("^");
	UpdateData(FALSE);
}

8.开方

// sqrt
void CEx6Dlg::OnBnClickedButton20()
{
	JUDGE_CALCATED
	op.push_back(OP_SQRT);
	showStr += TEXT("sqrt(");
	UpdateData(FALSE);
}

9.取余

// mod
void CEx6Dlg::OnBnClickedButton21()
{
	JUDGE_CALCATED
	op.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表示开方, 否则为左括号
	do
		switch (*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_STATE
			numStack.push_back(x);
			CASE_PM
			THROW_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_EXP
	numStack.push_back(x);
	CASE_PM
	CString 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


  1. 这里实际上是迭代器,其作用和指针类似,姑且算作指针。 ↩︎

  2. 这里考虑到后面的括号可能需要不同的查找下界,尽管最后没用上,这个opLowerBound参数还是保存了下来。 ↩︎

  3. 这里实际上用不到加减的计算,但由于后面的括号和结束运算时的需要,这里同一处理了加减。 ↩︎

  4. 哪怕是long long也会溢出。 ↩︎

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zsc_118

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值