(一)需求和规格说明
问题描述:设计并实现计算器。
编程任务:
- 利用栈计算器的底层原理,实现一个计算器程序。
- 用户可以在图形交互界面输入算式。
- 点击“=”后,计算结果呈现在图形交互界面上显示给用户。
- 增加3个逻辑运算符 &&、||、 !,并能处理逻辑运算符和算术运算符的混合运算。
- 有异常处理功能,并能及时反馈给用户。
(二)设计
1.问题分析
我们在基础框架上选择沿用STL实现的栈计算器,通过之前的学习我们知道栈计算器的原理是设计两个栈,一个是操作数栈,另一个是运算符栈。计算是根据运算符的优先级以及其特定的压栈弹栈规则调整计算顺序去实现的,而逻辑运算符也是如此,逻辑运算符也是运算符,只不过优先级要更高一点,而且计算结果是只有0和1的。所以说增加逻辑运算符其实是并未超出原有的框架的,这个的具体实现后面会详细讲。接着讨论可能是本次大作业最有意思的一个点,也就是图形交互功能,不过图形交互功能的实现也并不难,其实我原本是打算使用一些简单的HTML、CSS和JavaScript制作一个计算器静态网页来实现的,但是考虑到课程内范围学习的一直是C++,所以我最终还是选择使用C++来作为开发语言,不过除此之外确实还有一个很重要的原因,那就是C++跨平台图形用户交互界面开发工具Qt的存在。Qt的优点不必多说,首先Qt的用户交互界面开发我认为是比H5快的,Qt的ui界面可以直接拖动控件来制作,其次JS中注册事件函数,这个在Qt中就是信号函数和槽函数的概念,JS的注册事件函数是比较繁琐的,首先需要通过选择器来选中事件变量,再注册事件,而在图形交互界面只需要转到槽函数,就可以立即使用,去书写相当于事件函数的槽函数,这一点真的是特别节省时间,在这里特地夸一夸“转到槽”这个功能,真的是大大提高了Qt开发者的开发效率。还有一点就是减号是既可以作为单目运算符,也可以作为双目运算符的,这里的特殊处理我稍后也会提到。对于计算过程而言,原来的计算器是整数的,在此之上添加小数运算功能也并未脱离原有框架。最后一点是对于容错功能的添加,这个其实仍有待商榷,因为对于错误算式的枚举是比较困难的,所以我尽可能地列举了常见地几种错误算式,这一点后面会详细展开讲讲。综上所述,大作业(2)被分解为一个个我们可以着手解决的小问题,这确实应该使我们十分兴奋,由于之前有过一点点Qt的基础,做出来邀请同学进行测试时也测试出来不少问题,其中比较有趣同时也值得注意地几个问题就是:1.减号是既可以作为单目运算符也可以作为双目运算符,那么我们怎么判定它属于哪种情况,并且怎么修改计算原则,才能保证结果的正确性;2.double类型作为计算结果返回的科学计数法结果精度可能是有偏差的;3.之前列举的错误情况可能会导致一些正常的算式报错,形成矛盾,当然第3点可能完全是因为我个人对代码逻辑的掌控尚不过关,确实要感谢帮我测试的高中、大学同学们给我指出这些问题,他们不嫌弃我这个小新手制作的简陋计算器,每次都能给出让我“眼前一黑”的算式,然后我再修改半天,不过1.2这两点我觉得可能是更多同学都有可能会出现的问题,所以我会在详细分析中分享一下我的一些浅薄看法,那么对于问题的分析就到此结束。
2.设计思想
(1)首先构思一下该软件的各个方面的大致实现,以及规划一下需要设计的类及模块。由于该计算器程序的底层逻辑比较简单,所以我们不必要封装功能类,考虑到开发工具是QT,我们直接在widget.cpp文件中写计算函数和优先级比较函数的实现就可以了。
(2)按钮交互:用户在GUI上点击了按钮后,会自动调用编写好的槽函数,将算式进行相应的处理,并在GUI界面中呈现出来。
(3)计算:在点击“=”后,会自动调用错误算式检查函数和计算函数,然后将计算得到的结果呈现在GUI上。
3. 设计表示
- 项目结构:
QT本身在创建项目时,就有良好的项目管理结构,主要是分成两个文件夹,其中
build-Calculate_ver_wyuu- Desktop_Qt_5_14_2_MinGW_32_bit-Release文件夹中主要是应用程序的release,包含可执行程序等
另一个就是以项目名命名的文件夹
Calculate_ver_wyuu,其中包含了该项目的全部源代码,如果要添加一些资源文件,那么也是要将资源文件放到这一文件夹中的。
项目结构图如下所示:
项目结构图
代码目录结构图如下所示:
- 文件具体说明:
首先在Qt中创建项目,创建时基类选择了QWidget类,也就是最底层的基类,当然选择QMainWindow类也可以。我会首先在VS中对代码进行调试,然后再转到Qt中使用,因为Qt的Debug我还不是很熟练。
下面我将依次介绍代码的核心部分:
首先是widget头文件:widget类中的成员函数是对ui中所有QPushButton控件注册点击(Click()信号函数)事件后相应的槽函数,在widget中相应的进行声明,而数据成员是Ui::Widget *ui; QString s; QString ans;有输入的算式s和输出的结果ans。
#ifndef WIDGET_H #define WIDGET_H #include <QString>
#include <QWidget>
#include <string>
#include <iostream>
using namespace std;
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_3_clicked();
void on_pushButton_9_clicked();
void on_pushButton_10_clicked();
void on_pushButton_2_clicked();
void on_pushButton_7_clicked();
void on_pushButton_8_clicked();
void on_pushButton_clicked();
void on_pushButton_4_clicked();
void on_pushButton_5_clicked();
void on_pushButton_6_clicked();
void on_pushButton_15_clicked();
void on_pushButton_16_clicked();
void on_pushButton_17_clicked();
void on_pushButton_18_clicked();
void on_pushButton_20_clicked();
void on_pushButton_19_clicked();
void on_pushButton_22_clicked();
void on_pushButton_21_clicked();
void on_pushButton_14_clicked();
void on_pushButton_11_clicked();
void on_pushButton_12_clicked();
void on_pushButton_13_clicked();
void on_pushButton_50_clicked();
private:
Ui::Widget *ui;
QString s;
QString ans;
};
接下来是重点,也就是各个槽函数的实现,我们可以做如下分类:
<1>输入算式
其实输入运算符或是操作数时的槽函数都只是在单纯的让算式s+=输入的字符,然后通过TextEdit控件呈现出来,所以实际是同一类的,这里以输入1和减号为例:
void Widget::on_pushButton_3_clicked()
{
s+="1";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_16_clicked()
{
s+="-";
ui->TextEdit->setText(s);
}
<2>对算式进行清空和退格
清空:直接让s=””即可
void Widget::on_pushButton_22_clicked()
{
s="";
ui->TextEdit->setText(s);
}
退格:直接调用Qstring 类封装的chop()方法
void Widget::on_pushButton_21_clicked()
{
if(s.at(s.length()-1)=="&"||s.at(s.length()-1)=="|")
s.chop(2);
else
s.chop(1);
ui->TextEdit->setText(s);
}
<3>对’=’号的槽函数处理
void Widget::on_pushButton_14_clicked()
{
s=s+"#";
bool flag=checkwrong(s);
if(flag)
{
ui->TextEdit->setText("表达式错误,请重新输入");
}
else
{
double a= calculate(s);
QString str = QString::number(a,'f',8); //非科学记数法输出
ui->TextEdit->setText(str);
}
}
这里融汇了容错机制和计算机制,首先介绍计算机制,是在STL实现的栈计算器的基础上改进而来的:
运算符优先级函数:
int SL(char a)//优先级函数,给每个运算符赋予优先级
{
switch (a)
{
case '#':
return 0;
case '+':
return 2;
case '-':
return 2;
case '*':
return 3;
case '/':
return 3;
case '(':
return 1;
case ')':
return 1;
case '&':
return 5;
case '|':
return 4;
case '!':
return 6;
}
}
对逻辑运算符的优先级进行了处理,新增了三个逻辑运算符,并且值得注意的是此处&&的优先级大于||的优先级而小于单目运算符!的优先级,除此之外没什么新意,因为即便是逻辑运算符也还是在这个计算框架之下。
计算函数:
double transform(double x)
{
if (x == 0)
return 1;
else return 0;
}
这个是专门用来计算!运算符的。
下面进入重头戏:其中大部分都是沿用,但是会详细讲讲其中几个新的点
double calculate(string str)
{
stack<char> stack1;
stack<double> stack2;
string ss = "#";
str.insert(0, ss);
for (int i = 0;; i++)
{
if (str.at(i) >= '0' && str.at(i) <= '9')//操作数
{
double temp = str.at(i) - '0';
int num = 0;
int num1 = 0;
bool flag = false;
for (int n = 1;; n++)
{
if (str.at(i + n) >= '0' && str.at(i + n) <= '9')
{
temp = temp * 10 + (str.at(i + n) - '0');
num = n;
}
else
break;
}//处理整数的机制
if (str.at(i + num + 1) == '.')
{
for (int n = 1;; n++)
{
if (str.at(i + n + 1 + num) >= '0' && str.at(i + n + 1 + num) <= '9')
{
temp = temp + (str.at(i + num + 1 + n) - '0') * pow(10, -n);
num1 = n;
flag = true;
}
else
break;
}
}//处理小数的机制
stack2.push(temp);//如果扫描到的是操作数,直接入操作数栈
if (flag)
i = i + num + num1 + 1;
else
i = i + num;
}
else if (!(str.at(i) >= '0' && str.at(i) <= '9'))//运算符
{
if (i == 0)
{
stack1.push('#');
continue;
}
char x;
x = stack1.top();
if ((str.at(i) == '-' && str.at(i - 1) == '(') || (str.at(i) == '-' && str.at(i - 1) == '#'))
{
stack2.push(0);
}
if (str[i] == '&' && str[i + 1] == '&')
{
i++;
}
if (str[i] == '|' && str[i + 1] == '|')
{
i++;
}
if (str.at(i) == ')' && x == '(')
{
stack1.pop();//当)遇到(时,(弹栈
continue;
}
if (str.at(i) == '(')
{
stack1.push('(');//当前运算符是(,直接入栈
continue;
}
if (SL(str.at(i)) > SL(x))
{
stack1.push(str.at(i));//扫描到的是运算符,判断优先级,大于就入栈
}
if (str.at(i) == '#' && x == '#')
break;
if (SL(str.at(i)) <= SL(x))
{
if (stack1.top() == '!')
{
double val = transform(stack2.top());
stack1.pop();
stack2.pop();
stack2.push(val);
i--;
}
else
{
double b;
double c;
stack1.pop();
b = stack2.top();
stack2.pop();
c = stack2.top();
stack2.pop();
if (x == '+')
stack2.push(b + c);
else if (x == '-')
stack2.push(c - b);
else if (x == '*')
stack2.push(b * c);
else if (x == '/')
stack2.push(c / b);
else if (x == '&')
{
if (b != 0 && c != 0) stack2.push(1);
else stack2.push(0);
}
else if (x == '|')
{
if (b == 0 && c == 0) stack2.push(0);
else stack2.push(1);
}
i--;
}
}
}
}
double result;
result = stack2.top();
return result;
}
首先是Qt的特性,先将QString转为string,Qt也贴心的为我们准备了这样的接口。“.toStdString()方法”,然后就是沿用之前实现过的计算结构,这里有三个新的点:
1.逻辑运算符的引入,它的计算就是判断操作数栈顶上的元素和其相邻元素。结合一下&&、||的运算规则就完成了;
2.处理小数的话就是判断当前扫描的字符是否为小数点,是的话后面就用pow()函数将小数部分录入就完成了,最后要记录算式跳转了多少个字符。
if (str.at(i + num + 1) == '.')
{
for (int n = 1;; n++)
{
if (str.at(i + n + 1 + num) >= '0' && str.at(i + n + 1 + num) <= '9')
{
temp = temp + (str.at(i + num + 1 + n)-'0') * pow(10, -n);
num1 = n;
flag = true;
}
else
break;
}
}//处理小数的机制
其实与处理整数是一样的,我这里还写得有点复杂呢,有改进空间。
3.对于减号是单目运算符还是双目运算符的判断和计算的处理:
if ((str.at(i) == '-' && str.at(i - 1) == '(') || (str.at(i) == '-' && str.at(i - 1) == '#'))
{
stack2.push(0);
}
也就是说,如果遇到-,第一件事情不是直接进行运算符优先级的比较,而是去查看当前运算符的前一个是否还是运算符,比如如果遇到3+(-2)这种情况,减号前一个还是运算符,我们采取的方法是在操作数栈中压入一个0,将算式转化为3+(0-2),将单目运算处理为双目运算,对于+同理。不然的话因为没有处理这一层逻辑,会让计算器闪退。
至于其他的地方,由于我们在课后作业3和课后作业8中已经详细的探讨过,这里不再赘述。
4.关于容错机制,我在课后咨询过指导老师的意见,老师的意见是要对错误进行一定的分析,也就是我们要有这个意识,所以这里我枚举了几种常见的算式格式错误:
bool isoperator(char a)
{
bool flag=false;
if(a=='+'||a=='-'||a=='*'||a=='/'||a=='(')
flag=true;
return flag;
}
bool isoperator2(char a)
{
bool flag=false;
if(a=='*'||a=='/'||a=='+'||a=='-')
flag=true;
return flag;
}
bool checkwrong(QString s)//检验算式(部分性的)
{
bool flag=false;
string str = string((const char *)s.toLocal8Bit());
for(int i=0;i<str.length();i++)
{
//这里的i>0是因为-可能出现在第一位,要小心字符串越界
if(i>0&&((str.at(i)=='-'&&str.at(i-1)=='*')||(str.at(i)=='-'&&str.at(i-1)=='/')||(str.at(i)=='-'&&str.at(i-1)=='+')||(str.at(i)=='-'&&str.at(i-1)=='-')||(str.at(i)=='-'&&str.at(i-1)=='|')||(str.at(i)=='-'&&str.at(i-1)=='&')))
{//负数输入时应该带括号
flag=true;
}
if((str.at(i)=='('&&str.at(i+1)=='*')||(str.at(i)=='('&&str.at(i+1)=='/')||(str.at(i)=='('&&str.at(i+1)=='|')||(str.at(i)=='('&&str.at(i+1)=='&'))
{//左括号后应该不能是除了‘-’和‘+’以外的其他运算符
flag=true;
}
if(str.at(i)==')'&&isoperator(str.at(i-1)))
{//右括号)前应该是操作数而非(或运算符
flag=true;
}
if(str.at(i)==')'&&str.at(i+1)=='(')
{//右括号的后面不应该直接跟左括号
flag=true;
}
if((str.at(i)=='#'&& isoperator(str.at(i-1)))||(str.at(i)=='#'&& str.at(i-1)=='&')||(str.at(i)=='#'&& str.at(i-1)=='|'))
{//算式的末尾不应该是运算符或(
flag=true;
}
if(isoperator2(str.at(i))&&isoperator2(str.at(i+1)))
{//运算符不能连续出现,这里应该还有逻辑运算符不能连续出现的情况,那种情况下会闪退
flag=true;
}
//仍有一些错误没有枚举,例如左括号和右括号必须成对出现,出现这些错误时候会闪退,相当于报错了吧
}
return flag;
}
当然,对于错误的枚举确实是不够全面的,但是我将常见的算式错误枚举一二,没有枚举出来的会引发闪退,我想这也属于是一种报错吧。
5.QString str = QString::number(a,'f',8); //非科学记数法输出
之前在问题分析中也有提到double用科学计数法输出结果时会有精度的问题,所以我们选择使用非科学计数法输出,并在小数点后保留八位,以此保证结果的正确性。
接下来是UI设计的展示:
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
s=""; ans="";
ui->pushButton_3->setFont(QFont("宋体", 16));
ui->pushButton_9->setFont(QFont("宋体", 16));
ui->pushButton->setFont(QFont("宋体", 16));
ui->pushButton_2->setFont(QFont("宋体", 16));
ui->pushButton_10->setFont(QFont("宋体", 16));
ui->pushButton_7->setFont(QFont("宋体", 16));
ui->pushButton_8->setFont(QFont("宋体", 16));
ui->pushButton_5->setFont(QFont("宋体", 16));
ui->pushButton_4->setFont(QFont("宋体", 16));
ui->pushButton_6->setFont(QFont("宋体", 16));
ui->pushButton_15->setFont(QFont("宋体", 16));
ui->pushButton_16->setFont(QFont("宋体", 13));
ui->pushButton_17->setFont(QFont("宋体", 13));
ui->pushButton_18->setFont(QFont("宋体", 13));
ui->pushButton_14->setFont(QFont("宋体", 13));
ui->pushButton_11->setFont(QFont("宋体", 13));
ui->pushButton_12->setFont(QFont("宋体", 13));
ui->pushButton_13->setFont(QFont("宋体", 13));
ui->pushButton_50->setFont(QFont("宋体", 15));
ui->TextEdit->setFont(QFont("宋体", 20));
ui->TextEdit->setStyleSheet("QLabel{background-color:rgb(255,255,255);}");
setWindowTitle("Calculator_ver_wyuu");
}
UI做的也只是调大字体,设置TextEdit的背景颜色而已。而对于背景中的美学元素可以略作展开,画面的背景是出自一款叫做《明日方舟》的国产塔防游戏里面的人物“鸿雪”,她用两指捻起信件,不经意的侧颜体现出她的漫不经心和优雅,比起单调的计算器白色背景,这一背景图我认为明显提高了计算器的美感,体现出实用性和美观性在设计时都很重要。
4. 核心算法
(1)widget.cpp中核心算法的思路
double calculate(string str)
{
stack<char> stack1;
stack<double> stack2;
string ss = "#";
str.insert(0, ss);
for (int i = 0;; i++)
{
if (str.at(i) >= '0' && str.at(i) <= '9')//操作数
{
double temp = str.at(i) - '0';
int num = 0;
int num1 = 0;
bool flag = false;
for (int n = 1;; n++)
{
if (str.at(i + n) >= '0' && str.at(i + n) <= '9')
{
temp = temp * 10 + (str.at(i + n) - '0');
num = n;
}
else
break;
}//处理整数的机制
if (str.at(i + num + 1) == '.')
{
for (int n = 1;; n++)
{
if (str.at(i + n + 1 + num) >= '0' && str.at(i + n + 1 + num) <= '9')
{
temp = temp + (str.at(i + num + 1 + n) - '0') * pow(10, -n);
num1 = n;
flag = true;
}
else
break;
}
}//处理小数的机制
stack2.push(temp);//如果扫描到的是操作数,直接入操作数栈
if (flag)
i = i + num + num1 + 1;
else
i = i + num;
}
else if (!(str.at(i) >= '0' && str.at(i) <= '9'))//运算符
{
if (i == 0)
{
stack1.push('#');
continue;
}
char x;
x = stack1.top();
if ((str.at(i) == '-' && str.at(i - 1) == '(') || (str.at(i) == '-' && str.at(i - 1) == '#'))
{
stack2.push(0);
}
if (str[i] == '&' && str[i + 1] == '&')
{
i++;
}
if (str[i] == '|' && str[i + 1] == '|')
{
i++;
}
if (str.at(i) == ')' && x == '(')
{
stack1.pop();//当)遇到(时,(弹栈
continue;
}
if (str.at(i) == '(')
{
stack1.push('(');//当前运算符是(,直接入栈
continue;
}
if (SL(str.at(i)) > SL(x))
{
stack1.push(str.at(i));//扫描到的是运算符,判断优先级,大于就入栈
}
if (str.at(i) == '#' && x == '#')
break;
if (SL(str.at(i)) <= SL(x))
{
if (stack1.top() == '!')
{
double val = transform(stack2.top());
stack1.pop();
stack2.pop();
stack2.push(val);
i--;
}
else
{
double b;
double c;
stack1.pop();
b = stack2.top();
stack2.pop();
c = stack2.top();
stack2.pop();
if (x == '+')
stack2.push(b + c);
else if (x == '-')
stack2.push(c - b);
else if (x == '*')
stack2.push(b * c);
else if (x == '/')
stack2.push(c / b);
else if (x == '&')
{
if (b != 0 && c != 0) stack2.push(1);
else stack2.push(0);
}
else if (x == '|')
{
if (b == 0 && c == 0) stack2.push(0);
else stack2.push(1);
}
i--;
}
}
}
}
double result;
result = stack2.top();
return result;
}
(三)用户手册
(1)qt项目打包时,将动态链接文件也全部打包进去了,所以运行时可以直接运行,对方电脑中不需要下载对应的qt环境。
(2)运行软件时只需要运行,exe可执行程序即可。
(3)运行,exe后,程序会弹出一个主界面,主界面上有功能按钮可以点击进行算式的更改;点击“=”进行计算;
(4)计算结果会呈现在lineEdit上。
(5)带有一定的容错功能,如果遇到闪退现象,也是因为算式不正确而导致的。
(四)调试及测试
1. 部分程序运行截图
科学计数法输出的误差
4. 进一步改进
(1)目前程序想要运行时必须同时封装大量动态链接库,可以通过下载一些插件来避免。
(2)错误算式没有枚举完全。对于一些复杂的错误算式仍然会闪退。
(3)后期可以加上枚举类型,对每一种错误进行具体报错。
(五) 感想
没啥感想,大家看个乐子。不管做成啥样,老师的平时分都会打满的。
附录:
Widget.h头文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QString>
#include <QWidget>
#include <string>
#include <iostream>
using namespace std;
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_3_clicked();
void on_pushButton_9_clicked();
void on_pushButton_10_clicked();
void on_pushButton_2_clicked();
void on_pushButton_7_clicked();
void on_pushButton_8_clicked();
void on_pushButton_clicked();
void on_pushButton_4_clicked();
void on_pushButton_5_clicked();
void on_pushButton_6_clicked();
void on_pushButton_15_clicked();
void on_pushButton_16_clicked();
void on_pushButton_17_clicked();
void on_pushButton_18_clicked();
void on_pushButton_20_clicked();
void on_pushButton_19_clicked();
void on_pushButton_22_clicked();
void on_pushButton_21_clicked();
void on_pushButton_14_clicked();
void on_pushButton_11_clicked();
void on_pushButton_12_clicked();
void on_pushButton_13_clicked();
void on_pushButton_50_clicked();
private:
Ui::Widget *ui;
QString s;
QString ans;
};
#endif // WIDGET_H
Widget.cpp:
#include "widget.h"
#include "ui_widget.h"
#include <stack>
#include <QString>
#include <string>
#include <iostream>
#include <QPainter>
#include <QDebug>
#include <math.h>
using namespace std;
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
s=""; ans="";
ui->pushButton_3->setFont(QFont("宋体", 16));
ui->pushButton_9->setFont(QFont("宋体", 16));
ui->pushButton->setFont(QFont("宋体", 16));
ui->pushButton_2->setFont(QFont("宋体", 16));
ui->pushButton_10->setFont(QFont("宋体", 16));
ui->pushButton_7->setFont(QFont("宋体", 16));
ui->pushButton_8->setFont(QFont("宋体", 16));
ui->pushButton_5->setFont(QFont("宋体", 16));
ui->pushButton_4->setFont(QFont("宋体", 16));
ui->pushButton_6->setFont(QFont("宋体", 16));
ui->pushButton_15->setFont(QFont("宋体", 16));
ui->pushButton_16->setFont(QFont("宋体", 13));
ui->pushButton_17->setFont(QFont("宋体", 13));
ui->pushButton_18->setFont(QFont("宋体", 13));
ui->pushButton_14->setFont(QFont("宋体", 13));
ui->pushButton_11->setFont(QFont("宋体", 13));
ui->pushButton_12->setFont(QFont("宋体", 13));
ui->pushButton_13->setFont(QFont("宋体", 13));
ui->pushButton_50->setFont(QFont("宋体", 15));
ui->TextEdit->setFont(QFont("宋体", 20));
ui->TextEdit->setStyleSheet("QLabel{background-color:rgb(255,255,255);}");
setWindowTitle("Calculator_ver_wyuu");
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_3_clicked()
{
s+="1";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_9_clicked()
{
s+="2";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_10_clicked()
{
s+="3";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_2_clicked()
{
s+="4";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_7_clicked()
{
s+="5";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_8_clicked()
{
s+="6";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_clicked()
{
s+="7";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_4_clicked()
{
s+="8";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_5_clicked()
{
s+="9";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_6_clicked()
{
s+="0";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_15_clicked()
{
s+="+";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_16_clicked()
{
s+="-";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_17_clicked()
{
s+="*";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_18_clicked()
{
s+="/";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_20_clicked()
{
s+="(";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_19_clicked()
{
s+=")";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_22_clicked()
{
s="";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_21_clicked()
{
if(s.at(s.length()-1)=="&"||s.at(s.length()-1)=="|")
s.chop(2);
else
s.chop(1);
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_11_clicked()
{
s+="&&";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_12_clicked()
{
s+="||";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_13_clicked()
{
s+='!';
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_50_clicked()
{
s+='.';
ui->TextEdit->setText(s);
}
int SL(char a)//优先级函数,给每个运算符赋予优先级
{
switch (a)
{
case '#':
return 0;
case '+':
return 2;
case '-':
return 2;
case '*':
return 3;
case '/':
return 3;
case '(':
return 1;
case ')':
return 1;
case '&':
return 5;
case '|':
return 4;
case '!':
return 6;
}
}
double transform(double x)
{
if (x == 0)
return 1;
else return 0;
}
bool isoperator(char a)
{
bool flag=false;
if(a=='+'||a=='-'||a=='*'||a=='/'||a=='(')
flag=true;
return flag;
}
bool isoperator2(char a)
{
bool flag=false;
if(a=='*'||a=='/'||a=='+'||a=='-')
flag=true;
return flag;
}
bool checkwrong(QString s)//检验算式(部分性的)
{
bool flag=false;
string str = string((const char *)s.toLocal8Bit());
for(int i=0;i<str.length();i++)
{
//这里的i>0是因为-可能出现在第一位,要小心字符串越界
if(i>0&&((str.at(i)=='-'&&str.at(i-1)=='*')||(str.at(i)=='-'&&str.at(i-1)=='/')||(str.at(i)=='-'&&str.at(i-1)=='+')||(str.at(i)=='-'&&str.at(i-1)=='-')||(str.at(i)=='-'&&str.at(i-1)=='|')||(str.at(i)=='-'&&str.at(i-1)=='&')))
{//负数输入时应该带括号
flag=true;
}
if((str.at(i)=='('&&str.at(i+1)=='*')||(str.at(i)=='('&&str.at(i+1)=='/')||(str.at(i)=='('&&str.at(i+1)=='|')||(str.at(i)=='('&&str.at(i+1)=='&'))
{//左括号后应该不能是除了‘-’和‘+’以外的其他运算符
flag=true;
}
if(str.at(i)==')'&&isoperator(str.at(i-1)))
{//右括号)前应该是操作数而非(或运算符
flag=true;
}
if(str.at(i)==')'&&str.at(i+1)=='(')
{//右括号的后面不应该直接跟左括号
flag=true;
}
if((str.at(i)=='#'&& isoperator(str.at(i-1)))||(str.at(i)=='#'&& str.at(i-1)=='&')||(str.at(i)=='#'&& str.at(i-1)=='|'))
{//算式的末尾不应该是运算符或(
flag=true;
}
if(isoperator2(str.at(i))&&isoperator2(str.at(i+1)))
{//运算符不能连续出现,这里应该还有逻辑运算符不能连续出现的情况,那种情况下会闪退
flag=true;
}
//仍有一些错误没有枚举,例如左括号和右括号必须成对出现,出现这些错误时候会闪退,相当于报错了吧
}
return flag;
}
double calculate(string str)
{
stack<char> stack1;
stack<double> stack2;
string ss = "#";
str.insert(0, ss);
for (int i = 0;; i++)
{
if (str.at(i) >= '0' && str.at(i) <= '9')//操作数
{
double temp = str.at(i) - '0';
int num = 0;
int num1 = 0;
bool flag = false;
for (int n = 1;; n++)
{
if (str.at(i + n) >= '0' && str.at(i + n) <= '9')
{
temp = temp * 10 + (str.at(i + n) - '0');
num = n;
}
else
break;
}//处理整数的机制
if (str.at(i + num + 1) == '.')
{
for (int n = 1;; n++)
{
if (str.at(i + n + 1 + num) >= '0' && str.at(i + n + 1 + num) <= '9')
{
temp = temp + (str.at(i + num + 1 + n) - '0') * pow(10, -n);
num1 = n;
flag = true;
}
else
break;
}
}//处理小数的机制
stack2.push(temp);//如果扫描到的是操作数,直接入操作数栈
if (flag)
i = i + num + num1 + 1;
else
i = i + num;
}
else if (!(str.at(i) >= '0' && str.at(i) <= '9'))//运算符
{
if (i == 0)
{
stack1.push('#');
continue;
}
char x;
x = stack1.top();
if ((str.at(i) == '-' && str.at(i - 1) == '(') || (str.at(i) == '-' && str.at(i - 1) == '#'))
{
stack2.push(0);
}
if (str[i] == '&' && str[i + 1] == '&')
{
i++;
}
if (str[i] == '|' && str[i + 1] == '|')
{
i++;
}
if (str.at(i) == ')' && x == '(')
{
stack1.pop();//当)遇到(时,(弹栈
continue;
}
if (str.at(i) == '(')
{
stack1.push('(');//当前运算符是(,直接入栈
continue;
}
if (SL(str.at(i)) > SL(x))
{
stack1.push(str.at(i));//扫描到的是运算符,判断优先级,大于就入栈
}
if (str.at(i) == '#' && x == '#')
break;
if (SL(str.at(i)) <= SL(x))
{
if (stack1.top() == '!')
{
double val = transform(stack2.top());
stack1.pop();
stack2.pop();
stack2.push(val);
i--;
}
else
{
double b;
double c;
stack1.pop();
b = stack2.top();
stack2.pop();
c = stack2.top();
stack2.pop();
if (x == '+')
stack2.push(b + c);
else if (x == '-')
stack2.push(c - b);
else if (x == '*')
stack2.push(b * c);
else if (x == '/')
stack2.push(c / b);
else if (x == '&')
{
if (b != 0 && c != 0) stack2.push(1);
else stack2.push(0);
}
else if (x == '|')
{
if (b == 0 && c == 0) stack2.push(0);
else stack2.push(1);
}
i--;
}
}
}
}
double result;
result = stack2.top();
return result;
}
void Widget::on_pushButton_14_clicked()
{
s=s+"#";
bool flag=checkwrong(s);
if(flag)
{
ui->TextEdit->setText("表达式错误,请重新输入");
}
else
{
string str1=s.toStdString();
double a= calculate(str1);
QString str = QString::number(a,'f',8); //非科学记数法输出
s="";
ui->TextEdit->setText(str);
}
}
下面我将依次介绍代码的核心部分:
首先是widget头文件:widget类中的成员函数是对ui中所有QPushButton控件注册点击(Click()信号函数)事件后相应的槽函数,在widget中相应的进行声明,而数据成员是Ui::Widget *ui; QString s; QString ans;有输入的算式s和输出的结果ans。
#ifndef WIDGET_H
#define WIDGET_H
#include <QString>
#include <QWidget>
#include <string>
#include <iostream>
using namespace std;
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_3_clicked();
void on_pushButton_9_clicked();
void on_pushButton_10_clicked();
void on_pushButton_2_clicked();
void on_pushButton_7_clicked();
void on_pushButton_8_clicked();
void on_pushButton_clicked();
void on_pushButton_4_clicked();
void on_pushButton_5_clicked();
void on_pushButton_6_clicked();
void on_pushButton_15_clicked();
void on_pushButton_16_clicked();
void on_pushButton_17_clicked();
void on_pushButton_18_clicked();
void on_pushButton_20_clicked();
void on_pushButton_19_clicked();
void on_pushButton_22_clicked();
void on_pushButton_21_clicked();
void on_pushButton_14_clicked();
void on_pushButton_11_clicked();
void on_pushButton_12_clicked();
void on_pushButton_13_clicked();
void on_pushButton_50_clicked();
private:
Ui::Widget *ui;
QString s;
QString ans;
};
接下来是重点,也就是各个槽函数的实现,我们可以做如下分类:
<1>输入算式
其实输入运算符或是操作数时的槽函数都只是在单纯的让算式s+=输入的字符,然后通过TextEdit控件呈现出来,所以实际是同一类的,这里以输入1和减号为例:
void Widget::on_pushButton_3_clicked()
{
s+="1";
ui->TextEdit->setText(s);
}
void Widget::on_pushButton_16_clicked()
{
s+="-";
ui->TextEdit->setText(s);
}
<2>对算式进行清空和退格
清空:直接让s=””即可
void Widget::on_pushButton_22_clicked()
{
s="";
ui->TextEdit->setText(s);
}
退格:直接调用Qstring 类封装的chop()方法
void Widget::on_pushButton_21_clicked()
{
if(s.at(s.length()-1)=="&"||s.at(s.length()-1)=="|")
s.chop(2);
else
s.chop(1);
ui->TextEdit->setText(s);
}
<3>对’=’号的槽函数处理
void Widget::on_pushButton_14_clicked()
{
s=s+"#";
bool flag=checkwrong(s);
if(flag)
{
ui->TextEdit->setText("表达式错误,请重新输入");
}
else
{
double a= calculate(s);
QString str = QString::number(a,'f',8); //非科学记数法输出
ui->TextEdit->setText(str);
}
}
这里融汇了容错机制和计算机制,首先介绍计算机制,是在STL实现的栈计算器的基础上改进而来的:
运算符优先级函数:
int SL(char a)//优先级函数,给每个运算符赋予优先级
{
switch (a)
{
case '#':
return 0;
case '+':
return 2;
case '-':
return 2;
case '*':
return 3;
case '/':
return 3;
case '(':
return 1;
case ')':
return 1;
case '&':
return 5;
case '|':
return 4;
case '!':
return 6;
}
}
对逻辑运算符的优先级进行了处理,新增了三个逻辑运算符,并且值得注意的是此处&&的优先级大于||的优先级而小于单目运算符!的优先级,除此之外没什么新意,因为即便是逻辑运算符也还是在这个计算框架之下。
计算函数:
double transform(double x)
{
if (x == 0)
return 1;
else return 0;
}
这个是专门用来计算!运算符的。
下面进入重头戏:其中大部分都是沿用,但是会详细讲讲其中几个新的点
double calculate(string str)
{
stack<char> stack1;
stack<double> stack2;
string ss = "#";
str.insert(0, ss);
for (int i = 0;; i++)
{
if (str.at(i) >= '0' && str.at(i) <= '9')//操作数
{
double temp = str.at(i) - '0';
int num = 0;
int num1 = 0;
bool flag = false;
for (int n = 1;; n++)
{
if (str.at(i + n) >= '0' && str.at(i + n) <= '9')
{
temp = temp * 10 + (str.at(i + n) - '0');
num = n;
}
else
break;
}//处理整数的机制
if (str.at(i + num + 1) == '.')
{
for (int n = 1;; n++)
{
if (str.at(i + n + 1 + num) >= '0' && str.at(i + n + 1 + num) <= '9')
{
temp = temp + (str.at(i + num + 1 + n) - '0') * pow(10, -n);
num1 = n;
flag = true;
}
else
break;
}
}//处理小数的机制
stack2.push(temp);//如果扫描到的是操作数,直接入操作数栈
if (flag)
i = i + num + num1 + 1;
else
i = i + num;
}
else if (!(str.at(i) >= '0' && str.at(i) <= '9'))//运算符
{
if (i == 0)
{
stack1.push('#');
continue;
}
char x;
x = stack1.top();
if ((str.at(i) == '-' && str.at(i - 1) == '(') || (str.at(i) == '-' && str.at(i - 1) == '#'))
{
stack2.push(0);
}
if (str[i] == '&' && str[i + 1] == '&')
{
i++;
}
if (str[i] == '|' && str[i + 1] == '|')
{
i++;
}
if (str.at(i) == ')' && x == '(')
{
stack1.pop();//当)遇到(时,(弹栈
continue;
}
if (str.at(i) == '(')
{
stack1.push('(');//当前运算符是(,直接入栈
continue;
}
if (SL(str.at(i)) > SL(x))
{
stack1.push(str.at(i));//扫描到的是运算符,判断优先级,大于就入栈
}
if (str.at(i) == '#' && x == '#')
break;
if (SL(str.at(i)) <= SL(x))
{
if (stack1.top() == '!')
{
double val = transform(stack2.top());
stack1.pop();
stack2.pop();
stack2.push(val);
i--;
}
else
{
double b;
double c;
stack1.pop();
b = stack2.top();
stack2.pop();
c = stack2.top();
stack2.pop();
if (x == '+')
stack2.push(b + c);
else if (x == '-')
stack2.push(c - b);
else if (x == '*')
stack2.push(b * c);
else if (x == '/')
stack2.push(c / b);
else if (x == '&')
{
if (b != 0 && c != 0) stack2.push(1);
else stack2.push(0);
}
else if (x == '|')
{
if (b == 0 && c == 0) stack2.push(0);
else stack2.push(1);
}
i--;
}
}
}
}
double result;
result = stack2.top();
return result;
}
void Widget::on_pushButton_14_clicked()
{
s=s+"#";
bool flag=checkwrong(s);
if(flag)
{
ui->TextEdit->setText("表达式错误,请重新输入");
}
else
{
string str1=s.toStdString();
double a= calculate(str1);
QString str = QString::number(a,'f',8); //非科学记数法输出
s="";
ui->TextEdit->setText(str);
}
}