简易计算器的实现(MFC)

文章目录

    • 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++实现简单计算器(字符串替换)。由于字符串替换实现效率较低,因此在本实验中我们使用栈的方式实现。

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

  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_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


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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/605167.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机组成原理 I/O方式

I/O 方式 I/O方式分类: 程序查询方式。由 CPU通过程序不断查询 /O 设备是否已做好准备&#xff0c;从而控制0 设备与主机交换信息程序中断方式。只在 I/0 设备准备就绪并向 CPU发出中断请求时才予以响应。DMA方式。主存和 I/O 设备之间有一条直接数据通路&#xff0c;当主存和…

UDS诊断(ISO14229-1) 3D服务

文章目录 功能简介应用场景请求和响应1、请求2、子功能3、肯定响应4、否定响应 NRC 判断优先级顺序报文示例UDS中常用 NRC 功能简介 3D服务&#xff0c;即 WriteMemoryByAddress&#xff08;按地址写内存&#xff09;服务&#xff0c;允许客户端向服务器的一个或多个连续存储单…

开源游戏引擎:创造无限可能 | 开源专题 No.56

godotengine/godot Stars: 62.6k License: MIT Godot Engine 是一个功能强大的跨平台游戏引擎&#xff0c;可用于创建 2D 和 3D 游戏。它提供了一套全面的常见工具&#xff0c;让用户可以专注于制作游戏而不必重复造轮子。该引擎支持将游戏一键导出到多个平台上&#xff0c;包…

虾皮广告数据分析:如何进行虾皮广告数据分析以优化广告效果

虾皮&#xff08;Shopee&#xff09;作为一家知名的电商平台&#xff0c;广告数据分析是优化广告效果的关键步骤。通过对广告数据进行深入分析&#xff0c;卖家可以了解广告的表现、找出优势和不足&#xff0c;并制定更有效的广告策略。在本文中&#xff0c;我们将介绍如何进行…

Mac 16g约等于Windows多少g?

Mac 16g 内存等于 Windows 320g 内存 何为“黄金内存”&#xff1f; Mac 的内存是用黄金做的&#xff0c;而 Windows 的内存是用铁做的。 黄金的密度是 19.32 g/cm&#xff0c;而铁的密度是 7.874 g/cm。 因此&#xff0c;16g 的黄金体积是 0.082 cm&#xff0c;而 16g 的铁…

Vue3使用Pinia

1.安装 npm i pinia 2.搭建架子文件 2.1main.js import { createApp } from vue // 引入pinia import { createPinia } from piniaimport App from ./App.vue import router from ./router // 创建pinia const app createApp(App) //安装pinia app.use(createPinia())app.use(…

P1262 间谍网络

1、思路 阅读题目&#xff0c;发现有些间谍可以是被前面的点更新&#xff0c;也就是说&#xff0c;在一开始的时候&#xff0c;把能贿赂的人员从小到达排个序&#xff0c;再使用bfs算法&#xff0c;把他们能到达的人员的贿赂价钱设置为0。 有解的情况&#xff1a; 首先如果有…

【leetcode】力扣热门之反转链表【简单难度】

题目描述 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 用例 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1] 输入&#xff1a;head [] 输出&#xff1a;[…

强化学习的数学原理学习笔记 - 时序差分学习(Temporal Difference)

文章目录 概览&#xff1a;RL方法分类时序差分学习&#xff08;Temporal Difference&#xff0c;TD&#xff09;TD for state valuesBasic TD&#x1f7e1;TD vs. MC &#x1f7e6;Sarsa (TD for action values)Basic Sarsa变体1&#xff1a;Expected Sarsa变体2&#xff1a;n-…

教程:Centos6迁移旧虚拟机文件后的网络配置教程,完美解决虚拟机移动后的网络ip变化问题

博主在工作后,想整整之前大学的虚拟机集群,因此特意从之前的旧电脑把虚拟机文件给拷贝了过来,在导入到vm-workstation,顺便能启动虚拟机后,发现之前的静态ip已经跟现在的宿主机网络不一样。想着重新配置,但觉得太麻烦,故想到了修改网卡的mac地址+网卡重配置方法,完美解…

【已解决】如何用c语言位运算输出浮点数数据

本博文源于笔者正在学习的c语言如何利用位运算输出浮点数数据类型&#xff0c;浮点数在其底部也是用二进制来处理&#xff0c;又考虑到他是低位在前高位在后的原理&#xff0c;因此进行了这样的代码编写 问题浮现 想要用c语言的位运算输出浮点数 问题源码 #include<stdi…

etcd储存安装

目录 etcd介绍: etcd工作原理 选举 复制日志 安全性 etcd工作场景 服务发现 etcd基本术语 etcd安装(centos) 设置&#xff1a;etcd后台运行 etcd 是云原生架构中重要的基础组件&#xff0c;由 CNCF 孵化托管。etcd 在微服务和 Kubernates 集群中不仅可以作为服务注册…

[大厂实践] 重新发明后端子集

子集算法有助于优化服务间连接利用率&#xff0c;降低资源使用。但随机或轮询子集算法在动态拓扑环境中会造成较高的连接扰动。本文介绍了谷歌在解决连接扰动方面所做的思考和实践&#xff0c;并介绍了当前最新的Rocksteadier子集算法。原文: Reinventing Backend Subsetting a…

面试题:怎么给详情页做性能优化的?

文章目录 一、背景二、接口优化方案总结1.批处理2.异步处理3.空间换时间4.预处理5.池化思想6.串行改并行7.索引8.避免大事务9.优化程序结构10.深分页问题11.SQL优化12.锁粒度避免过粗 三、最后 一、背景 针对老项目&#xff0c;去年做了许多降本增效的事情&#xff0c;其中发现…

03MyBatis完成CRUD

准备工作 ○ 创建module&#xff08;Maven的普通Java模块&#xff09;&#xff1a;mybatis-002-crud ○ pom.xml ■ 打包方式jar ■ 依赖&#xff1a; ● mybatis依赖 ● mysql驱动依赖 ● junit依赖 ● logback依赖 ○ mybatis-config.xml放在类的根路径下 ○ CarMapper.xml放…

Elasticsearch:Serarch tutorial - 使用 Python 进行搜索 (二)

这个是继上一篇文章 “Elasticsearch&#xff1a;Serarch tutorial - 使用 Python 进行搜索 &#xff08;一&#xff09;” 的续篇。在今天的文章中&#xff0c;我们接着来完成如何进行分页及过滤。 分页 - pagination 应用程序处理大量结果通常是不切实际的。 因此&#xff0…

【ros笔记】urdf文件

urdf文件属于xml文件&#xff0c;他的标签有&#xff1a; <robot name"robot_name"><!-- 看的见摸的着刚体用link --><link name"base_link"><!-- 可视化部分 --><visual><!-- 几何形状 --><geometry><!-- b…

vue-springboot 音乐推荐系统 带歌词的音乐播放器系统设计与实现 7902c

少数民族音乐网站在流畅性&#xff0c;续航能力&#xff0c;等方方面面都有着很大的优势。这就意味着少数民族音乐网站的设计可以比其他系统更为出色的能力&#xff0c;可以更高效的完成最新的音乐信息、音乐资讯、在线交流等功能。 此系统设计主要采用的是JAVA语言来进行开发&…

基于SpringBoot的教学辅助系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

实战Flink Java api消费kafka实时数据落盘HDFS

文章目录 1 需求分析2 实验过程2.1 启动服务程序2.2 启动kafka生产 3 Java API 开发3.1 依赖3.2 代码部分 4 实验验证STEP1STEP2STEP3 5 时间窗口 1 需求分析 在Java api中&#xff0c;使用flink本地模式&#xff0c;消费kafka主题&#xff0c;并直接将数据存入hdfs中。 flin…