使用C++完成以栈为基础的简易计算器,并使用Qt5制作可视化界面
一.计算器内部逻辑构造:
在完成计算器时选用栈数据结构(自己编写的和标准模板库中的都可),要求支持加减乘除和逻辑与或非运算,可计算整数和小数。这一部分有很多思路都可以实现,我的写法是将整个步骤分为表达式抽离,出入栈操作和运算符运算三个步骤。
1.表达式抽离
首先,在没有做出交互界面的情况下我们假定用户以英文键盘输入所要计算的表达式,这时你就会得到一个字符串,为了将字符串中每一个数字或运算符进行分割抽离,我们可以选用一个vector容器储存单独抽离出的每一个数字或运算符,具体解决思路如下:
1.设置一个储存字符串类型的vector v;并设置一个用于暂时储存数字的字符串number。
2.开始对字符串进行抽离,总体原则如下:
使用循环对每个字符进行判断,如果是数字,就将其放进number中暂时储存起来,接着对下一个字符进行判断:如果下一个字符是依旧是数字,就说明用户输入的数字大于9,继续将他放入number中即可。如果下一个字符不是数字,就需要将目前number中储存的数字放进v中,并将number清空。(注意:因为要实现小数运算,所以当字符不是数字时应该判断是否是小数点,如果是小数点就应该将其放入number中,算作这个数字的一部分。)如果判断不是小数点,将其放入v中即可。(注意,对于&&与 || 两个运算符,为了处理方便,我们只将&和 | 放入v中,同时,因为这两个符号在字符串中占两个位置,所以在处理这两个符号后应前推一个字符。)
补充特例:取反运算符“ !”;对于之前所有运算符和数字,我们将其抽离后都是放入栈中运算(具体后面会讲到),但是对于“!”运算符,因为他是单目运算符,所以我不想把他放入栈中(我也不知道当时怎么想的,可能脑子抽了),所以在当字符不是数字时需要判断当前字符不是”!“时才可按照上述原则处理符号。当检测到这个字符是”!“时应该怎么办呢?我们可以对可能出现的情况进行分类:
1.如果”!“后是数字,那么就单独设置一个字符串s,按照之前的处理原则,将整个数字放入s中,并将其转变为数字,将这个数字进行取反运算,并将得到的结果放入number中,就相当于将”!xxx“整体看作了0或1,当作数字抽离。
2.如果”!“后是括号,我们则需要计算括号中整体的运算结果后再与”!“进行运算,对于这一步,我们需要找到”!“后左括号所对应控制的右括号,并将括号以及其内容整体进行抽离,这时可以设置一个字符串对”!“所支配的表达式进行储存,考虑到可能出现如!((1))等多层括号嵌套的情况,我们可以设置参数a和b来分别储存左右括号数,当括号数相同时进行抽离。这时,我们就已经得到了”!"所控制的内容,因为我们不知道这一部分内容的复杂度,所以我们可以将其放入evaluate()(后面会看到)中重新对这一部分进行抽离,出入栈并进行运算得到结果(这里采用了递归的方法,减少了需要我们处理的部分)最后将所得结果进行取反运算,转化为0或1放入number即可。
至此,我们的抽离运算已经完成,下面贴上代码:
vector<string>split(const string& expression)//处理储存表达式{
vector<string> v;
string number;for(int i=0; i< expression.length(); i++){if(isdigit(expression[i]))
number.append(1, expression[i]);else{if(number.size()>0&&(expression[i]!='!')){if(!isspace(expression[i])&&(expression[i]=='.'))//处理小数点
number.append(1, expression[i]);else{
v.push_back(number);
number.erase();}}if(!isspace(expression[i])&&(expression[i]=='|')){
string a;
a.append(1, expression[i]);
v.push_back(a);
i++;}if(!isspace(expression[i])&&(expression[i]=='&')){
string b;
b.append(1, expression[i]);
v.push_back(b);
i++;}if(!isspace(expression[i])&&(expression[i]!='.')&&(expression[i]!='!')&&(expression[i]!='|')&&(expression[i]!='&')){
string s; s.append(1, expression[i]);
v.push_back(s);}if(!isspace(expression[i])&&(expression[i]=='!')){if(isdigit(expression[i+1])){
string s;while(expression[i+1]=='.'||isdigit(expression[i+1])){
s.append(1, expression[i+1]);
i++;}int a=!atof(s.c_str());if(a==0)
number.append(1,'0');elseif(a==1)
number.append(1,'1');}if(expression[i+1]=='('){
string s;int head=1;int tail=0;
s.append(1,'(');
i++;while(tail!= head){
s.append(1, expression[i+1]);if(expression[i+1]=='(')
head++;elseif(expression[i+1]==')')
tail++;
i++;}int a=!(evaluate(s));if(a==0)
number.append(1,'0');elseif(a==1)
number.append(1,'1');}}}}if(number.size()>0)
v.push_back(number);return v;}
2.对抽离后的表达式进行入栈并计算
这一部分很多数据结构的书上都会讲,这里就不再多说,只需要注意各个运算符的逻辑顺序即可,我这里将出入栈操作与运算操作分开写为两个函数compute和evaluate,其中evaluate包含抽离函数split和计算函数compute,以一个字符串为参数,最终只需调用evaluate函数即可。最后输出结果为double类型。
下面直接贴上代码:
doubleevaluate(const string& expression)//进行栈处理{
LStack<double> operandStack;
LStack<char> operatorStack;
vector<string> evaluate_posture=split(expression);for(int i=0; i< evaluate_posture.size(); i++){if(evaluate_posture[i][0]=='+'|| evaluate_posture[i][0]=='-'){while(!operatorStack.empty()&&(operatorStack.get_top()=='+'|| operatorStack.get_top()=='-'|| operatorStack.get_top()=='*'|| operatorStack.get_top()=='/'|| operatorStack.get_top()=='|'|| operatorStack.get_top()=='&')){compute(operandStack, operatorStack);;}
operatorStack.Push(evaluate_posture[i][0]);}elseif(evaluate_posture[i][0]=='*'|| evaluate_posture[i][0]=='/'){while(!operatorStack.empty()&&(operatorStack.get_top()=='*'|| operatorStack.get_top()=='/'|| operatorStack.get_top()=='|'|| operatorStack.get_top()=='&')){compute(operandStack, operatorStack);}
operatorStack.Push(evaluate_posture[i][0]);}elseif(evaluate_posture[i][0]=='|'){while(!operatorStack.empty()&&(operatorStack.get_top()=='|'|| operatorStack.get_top()=='&')){compute(operandStack, operatorStack);}
operatorStack.Push(evaluate_posture[i][0]);}elseif(evaluate_posture[i][0]=='&'){while(!operatorStack.empty()&&(operatorStack.get_top()=='&')){compute(operandStack, operatorStack);}
operatorStack.Push(evaluate_posture[i][0]);}elseif(evaluate_posture[i][0]=='('){
operatorStack.Push('(');}elseif(evaluate_posture[i][0]==')'){while(operatorStack.get_top()!='('){compute(operandStack, operatorStack);}
operatorStack.pop();}else{
operandStack.Push(atof(evaluate_posture[i].c_str()));}}while(!operatorStack.empty()){compute(operandStack, operatorStack);}return operandStack.pop();}
下面是计算部分的代码:
voidcompute(LStack<double>& operandStack, LStack<char>& operatorStack)//运算符分别进行计算{char x= operatorStack.pop();double a= operandStack.pop();double b= operandStack.pop();if(x=='+')
operandStack.Push(b+ a);if(x=='-')
operandStack.Push(b- a);if(x=='*')
operandStack.Push(b* a);if(x=='|'){
operandStack.Push(a|| b);}if(x=='&'){
operandStack.Push(a&& b);}if(x=='/'){if(a==0)
cout<<"错误,零不能作分母"<< endl;else
operandStack.Push(b/ a);}}
到这里,计算器内部的逻辑结构基本上就已经实现,经过测试,此计算器可以完整的计算一些简单运算,下面给出两个典型例子:
有一点需要注意,我用的栈是自己写的,和标准模板库中的有一点不同,那就是我的pop函数会返回栈顶元素,标准模板库中的却不会,如果你使用的是标准模板库中的栈类型,需要对这一点进行更改。
使用Qt做出图形界面
这一点相信主要是工大的可怜孩子们有这样的需求,我也没有系统的学习Qt,主要是看了几节视频和借鉴同学写的博客,至于基础的操作,我觉得下面这篇博客写的较为清楚,大家可以直接去看这一篇,我这里主要是对制作过程中遇到的一些问题(坑)进行介绍。
下面先贴上博客链接:https://blog.csdn.net/yogur_father/article/details/106222926?utm_source=app
下面介绍一些我遇到的问题,如果你也遇到了和我同样的问题,希望可以为你提供一些帮助。
1.添加背景图片
添加背景图片的方法,在上面贴出的博客里已经有了介绍,简单来说就是在设计界面使用styleSheet进行背景图片的添加,在添加时,background-image和border-image都会改变原图片的尺寸,只有image不会,这里我选用的是image。需要注意的是,对于Qwidget类创建的主窗口进行操作时,所添加的图片在运行后的计算器界面中并没有出现,当我将按键设置为透明后,反倒是点击按键会显示添加的图片,对于这个问题的解决,可以从设计界面添加一个Frame覆盖住整个窗口,对这个Frame进行设置背景图片,然后单击右键选择放到后面,将其置于按钮下方即可。
2.按键透明化
有了漂亮的背景,怎么能被丑丑的按键挡住呢,要设置透明的按键,主要有两步:
1.对于选中的按钮,将左下角属性中的Flat选中
2.右键单击选中的按钮->改变样式表->在文本框中输入:background:transparent;即可
同样的,可以使用styleSheet改变字体的大小。
3.弹窗报错机制的实现
之前我们的目的是做出可交互的图形界面,那么就要考虑到一些错误输入的情况,理想的做法是:每当用户进行错误的输入时,我们可以设置一个弹窗来提醒用户。由于并没有深入的学习Qt,我在经过查阅后选择了最简单的实现需求的方法,具体说明如下:
在Qt中内置了一个QMessageBox的类型,它的作用就是创造一个警告弹窗,你可以自行选择弹窗中的内容,对于我这种初学者来说这个类真的超赞,可以说用最少的代码量实现了我想要的功能具体实现如下:
#include<QMessageBox>//包含对应头文件
QMessageBox warnning;//创造对象
warnning.setText("请放心!");//设置提醒内容
warnning.exec();
到了这里,我们已经学会了如何创造一个弹窗,你只需要在用户进行错误输入的时候进行提醒即可,下面举一个例子:当用户没有输入表达式直接点击“=”时进行提醒:当用户进行错误输入时就会有弹窗提醒:
当然,可能出现的异常输入有很多,你可以对每一种错误输入设置单独的提醒,这里就不再多说。
4.小数输出
等号的实现思路与注意事项在我先前贴出的那个博客里已经提到,这里需要注意的是我们的计算器可以计算小数,那么小数就要有保留位数,设置输出位数的代码如下:
string abc=sss.toStdString();//将Qstring变为stringdouble aaa=evaluate(abc);//得到计算结果
res=QString::number(aaa,10,4);//将浮点数转化为Qstring并设置为小数点后四位。
ui->textEdit_2->setText(res);//显示结果
5.exe打包
好了,现在我们的计算器就已经基本上完成并且可以运行了,不出意外的话,我想你一定会想将整个程序打包,得到一个可以单独运行的exe文件,巧了,我也一样,对于这一过程的实现过程,下面这篇博客有详细的介绍,我就不在多说,下面是链接:
https://blog.csdn.net/windsnow1/article/details/78004265?utm_source=app
到这里,整篇教程应该就已经完成啦(抹一把辛酸泪),如果你觉得对你有帮助的话,就点个赞吧(◍˃̶ᗜ˂̶◍)✩