开发一个支持四则运算且能识别错误计算式的计算器
计算器示例界面设计:
以QWidget对象作为窗口,子组件为QLineEdit对象与QPushButton对象
组件间间隔10px,按钮宽40px、高40px,文本框组件宽5 * 40px + 4 *10px、高30px
去除窗口的最小、大化按钮
固定窗口大小
禁止文本框直接输入
界面元素架构设计:
使用面向对象的继承、组合关系架构计算器
核心算法设计:
为了使计算器程序支持四则运算使用了三个算法:
一、将中缀表达式进行数字与运算符的分离
二、将中缀表达式转换为后缀表达式
三、计算后缀表达式得到最终结果
分离:
/******************************************************************************************
分离算法分析:
目的:将一串混合数字与符号的字符串分离成多个子串,每一个子串是一个数字或 运算符
分辨数字、正负号、运算符:
字符的值满足'0'~'9' 或 '.'时字符必然是一个数字
当一个符号(字符为'+' '-' '*' '/' '(' ')'时为符号)满足三个条件时,该符号为正负号:
(1)、符号为首字符
(2)符号前一个字符是左括号'('
(3)、符号前一个字符是字符'+' '-''*' '/'
否则符号(字符为'+' '-' '*' '/' '(' ')'时为符号)为运算符
具体实现分析:
遍历待分离字符串的每一个元素,
如果当前字符为数字则纳入字符串对象存储
如果当前字符是符号,则:
(1)、判断一次存储数字的对象是否为空,不为空时则说明对象已经存储了一个完整的数字,则将对象插入队列保存
(2)、判断当前的符号是正负号还是运算符,是正负号则纳入存储数字的对象,否则是运算符,则直接插入队列
将存储数字的对象插入队列(对象中可能存有最后一个数字)
*******************************************************************************************/
代码:
QQueue<QString> QCalculatorDec::split(constQString& exp)
{
QQueue<QString> ret;
QString num = ""; //保存分离出来的数字,数字可以是整数、浮点数、负数、整数
QString pre = ""; //当前字符的前一个字符,需要通过前一个字符的情况判断当前字符是正负号还是运算符
for(int i=0; i<exp.length(); i++) //遍历字符串exp的每一个字符
{
if( isDigitOrDot(exp[i]) ) //当前字符是数字或小数点
{
num += exp[i]; //当前字符是数字或者小数点就放入对象num保存
pre = exp[i]; //将当前字符(数字或小数点)存入变量pre,以便遍历到下一个字符的时候通过此字符判断
}
else if( isSymbol(exp[i]) ) //如果当前字符是符号
{//当前是符号时,前一个符号要么为空(当前符号为正负号时),要么是一个完整的数字(当前符号为运算符时)
if( !num.isEmpty() )
{
ret.enqueue(num); //将存有数字的对象num存入队列
num.clear(); //清空用于保存字符串的num,以便下一次存储数字
}
//必须定义一个字符串对象pre去保存前一个字符,而不是[i-1]因为[i-1]可能发生前越界
if( isSign(exp[i]) && ((pre == "") || (pre =="(") || isOperator(pre)) )
{ //如果当前是‘+’或‘-’并且不存在前一个字符或前一个字符为"("或操作符时,当前'+'或'-'为正负号,纳入num保存
num += exp[i];
}
else //当前的符号为运算符,单独插入队列保存
{
ret.enqueue(exp[i]);
}
pre = exp[i]; //将当前字符(符号)存入对象pre,便于遍历到下一个字符的时候通过此字符判断
}
}
if( !num.isEmpty() ) //遍历完成之后num中可能存在内容(数字),需判断
{
ret.enqueue(num); //对象num不为空时,将num中的内容插入队列
}
return ret;
}
中缀转后缀:
中缀转后缀的前提是中缀表达式中的括号应该匹配
/***********************************************************************************
匹配算法分析:
目的:判断包含数字与运算符的队列对象中的左右括号是否匹配
判断匹配依据:
匹配:左括号应该先于右括号出现,且左括号数量与右括号数量相等
不匹配:(1)右括号先于左括号出现、(2)左括号与右括号的数量不相等
具体实现分析:
如果左右括号匹配,必然的,左括号先于右括号出现、且一个左括号会对应一个右括号,使用栈与队列来判断括号的匹配性
遍历队列中每个元素,如果遍历到左括号则入栈,由于算式中的右括号必然出现在左括号之后,因此当遍历到右括号时,栈顶应该存在左括号,否则说明右括号先于左括号出现,不匹配情况一
如果遍历到右括号且栈顶元素是左括号时,弹出栈顶的左括号,当遍历完成时,如果左右括号匹配,那么栈应该为空,否则说明左括号数量多于右括号,不匹配情况二
转换算法分析:
目的:在队列对象中左右括号匹配的情况下将中缀表达式转化为后缀表达式
中缀转后缀的关键:
去除括号的情况下保留运算顺序
转换后可以顺序计算出最终结果
转换前后数字出现次序不变
运算符出现次序依赖于实际运算顺序
具体实现分析:
利用栈先入后出的特性控制运算顺序,使用栈与队列来完成中缀到后缀的转换
循环弹出中缀表达式队列中的每一个元素直到队列为空;
(1)、如果当前弹出元素为数字,为保证转换前后数字出现次序不变,直接输出数字;
(2)、如果当前弹出元素为运算符(运算符('+' '-''*' '/')有优先级,要保证优先级高的运算符先输出,但需要比较才能知道优先级),需要将当前弹出运算符入栈以便于与此后弹出的运算符比较优先级,当栈中有运算符时,当前弹出运算符应该与栈中的运算符做优先级的比较,为保证优先级高的运算符先输出,应该循环输出比当前弹出运算符优先级高的栈顶运算符直到低于当前弹出运算符,此时将当前弹出运算符入栈,即便当前弹出的所有运算符优先级都高于栈顶运算符,由于栈的特定,优先级高的运算符也会始终在栈顶,即优先级高的运算符始终会先输出
(3)、如果当前弹出元素为左括号,则入栈
(4)、如果当前弹出元素为右括号,则循环输出栈中运算符直到左括号(以左右括号为标志输出栈顶元素是为了区分括号作用部分与括号未作用部分),此时弹出左括号(丢弃)
(5)、如果弹出元素不属于数字或者运算符,则说明出现了其他符号,应该提示错误,并保证输出为空(错误之后无需计算)
输出栈中所有剩余运算符(由于输出的条件的原因,栈中可能还存在运算符)
***********************************************************************************/
代码:
boolQCalculatorDec::match(QQueue<QString>& exp)
{
bool ret = true;
int len = exp.length();
QStack<QString> stack; //栈用于存储左括号
for(int i=0; i<len; i++) //遍历队列中的每个QString
{
if( isLeft(exp[i]) )
{
stack.push(exp[i]); //当前元素是左括号入栈 }
else if( isRight(exp[i]) ) //当前元素是右括号时
{
if( !stack.isEmpty() && isLeft(stack.top()) ) //栈顶是左括号时
{
stack.pop(); //将栈顶的左括号弹出,发现一次右括号就应该弹一次栈中的左括号
}
else
{
ret = false; //当前元素是右括号,栈顶却不是左括号(栈为空),右括号先于左括号出现,括号不匹配
break; //已经发现不匹配,无需继续循环
}
}
}
return ret && stack.isEmpty(); //返回时栈必须为空,否则说明括号不匹配,左括号多于右括号
}
bool QCalculatorDec::transform(QQueue<QString>&exp, QQueue<QString>& output)
{
bool ret = match(exp); //首先检查括号是否匹配
QStack<QString> stack; //栈用于存储运算符与括号
output.clear();
while( ret && !exp.isEmpty() )
{
QString e = exp.dequeue();
if( isNumber(e) )
{
output.enqueue(e); //取出的头元素是数字,将元素入队到输出队列
}
else if( isOperator(e) ) //取出的头元素是符号则需要对比优先级
{ //当前元素优先级小于栈顶时循环弹出栈中比当前元素优先级高的符号到输出队列中
while( !stack.isEmpty() && (priority(e) <=priority(stack.top())) )
{
output.enqueue(stack.pop());
}
stack.push(e); //将头元素入栈以便下次比较
}
else if( isLeft(e) ) //头元素是左括号
{
stack.push(e); //将头元素入栈
}
else if( isRight(e) ) //头元素是右括号
{ //栈顶元素不是左括号时循环将栈顶元素弹出到输出队列中
while( !stack.isEmpty() && !isLeft(stack.top()) )
{
output.enqueue(stack.pop());
}
if( !stack.isEmpty() ) //操作栈时栈需不为空
{
stack.pop(); //弹出栈顶的左括号
}
}
else
{
ret = false; //出现其他符号,错误
}
}
while( !stack.isEmpty() ) //因为决定出栈的判断方式,所以栈内可能存有运算符
{
output.enqueue(stack.pop());
}
if( !ret ) //遇到错误时应该将输出队列清空
{
output.clear();
}
return ret;
}
计算:
/********************************************************************************************************
计算算法分析:
目的:计算后缀表达式的值,得出的结果以浮点的形式显示
计算的关键:
左右操作数的顺序,这一顺序对减法与除法尤其重要
计算完毕时,栈中只能存在一个元素,且唯一元素数字
计算时,如果运算符是除号,应该考虑除0的情况,0不能为除数
由于浮点数在内存中的表示是不精确的,不能直接拿数字与0作比较
具体实现分析:
使用栈与队列完成对后缀表达式值的计算
循环弹出后缀表达式队列中的每一个元素直到队列为空;
(1)、如果弹出的元素是数字,将数字直接入栈;
(2)、如果弹出的是运算符且栈不为空则将栈顶的元素弹出作为右操作数,若栈仍不为空则再一次弹出栈顶元素作为左操作数,并根据左右操作数与运算符计算结果,如果计算结果有效则将结果入栈,否则无需继续循环,并返回错误
(3)、如果弹出的元素既不是数字也不是运算符则说明混入了其他字符,跳出循环并返回错误
后缀表达式在计算完成后栈中应该只有唯一的一个元素,且元素应该为数字,满足条件则返回值应该为计算结果,否则返回值为错误
********************************************************************************************************/
代码:
QStringQCalculatorDec::calculate(QQueue<QString>& exp)
{
QString ret = "Error";
QStack<QString> stack; //存储操作数的栈
//队列不为空时循环取出队列头元素
while( !exp.isEmpty() )
{
QString e = exp.dequeue();
if( isNumber(e) )
{
stack.push(e); //头元素是数字直接入栈
}
else if( isOperator(e) ) //头元素是操作符
{
QString rp = !stack.isEmpty() ? stack.pop() : "";//弹出栈顶元素作右操作数
QString lp = !stack.isEmpty() ? stack.pop() : "";//再次弹出栈顶元素作左操作数
QString result = calculate(lp, e, rp); //调用实际计算函数
if( result != "Error" )
{
stack.push(result); //计算结果正确则将结果入栈
}
else
{
break; //计算结果错误则跳出循环
}
}
else //含有其他字符
{
break;
}
}
//当后缀表达式队列元素全部被处理且栈中唯一的元素为数字时,栈中的结果为有效的运算结果
if( exp.isEmpty() && (stack.size() == 1) &&isNumber(stack.top()) )
{
ret = stack.pop();
}
return ret;
}
实际计算函数:
QString QCalculatorDec::calculate(QStringl, QString op, QString r)
{
QString ret = "Error";
if( isNumber(l) && isNumber(r) ) //左右操作数有效时进行运算
{
double lp = l.toDouble();
double rp = r.toDouble();
if( op == "+" )
{
ret.sprintf("%f", lp +rp);
}
else if( op == "-" )
{
ret.sprintf("%f", lp - rp);
}
else if( op == "*" )
{
ret.sprintf("%f", lp * rp);
}
else if( op == "/" ) //当操作数为除法时,需要注意右操作数不能为0 即除数不为0
{
//浮点数在内存的表示是不精确的,所以需要定义一个足够小的比较变量
const double P = 0.000000000000001;
if( (-P < rp) && (rp < P) ) //如果右操作数在一个足够小的比较变量的范围内,将右操作数视为0
{
ret = "Error";
}
else
{
ret.sprintf("%f", lp/ rp);
}
}
else //其他符号
{
ret = "Error";
}
}
return ret;
}
程序整体架构设计:
用户界面模块(QCalculatorUI)与业务逻辑模块(QCalculatorDec)之间相互独立,仅仅通过接口(ICalculator)相互关联,用户界面模块(QCalculatorUI)使用接口(ICalculator),业务逻辑模块(QCalculatorDec)实现接口(ICalculator),QCalculator类封装用户界面模块与业务逻辑模块。
用户界面:
为避免半成品对象,使用二阶构造
实现:
QCalculatorUI::QCalculatorUI() :QWidget(NULL, Qt::WindowCloseButtonHint)
{
m_cal = NULL; //m_cal是指向接口类的指针
}
bool QCalculatorUI::construct()
{
bool ret = true;
const char* btnText[20] =
{
"7", "8", "9", "+","(",
"4", "5", "6", "-",")",
"1", "2", "3", "*","<-",
"0", ".", "=", "/","C",
};
m_edit = new QLineEdit(this);
if( m_edit != NULL )
{
m_edit->move(10, 10);
m_edit->resize(240, 30);
m_edit->setReadOnly(true);
m_edit->setAlignment(Qt::AlignRight);
}
else
{
ret = false;
}
for(int i=0; (i<4) && ret; i++)
{
for(int j=0; (j<5) && ret; j++)
{
m_buttons[i*5 + j] = new QPushButton(this);
if( m_buttons[i*5 + j] != NULL )
{
m_buttons[i*5 +j]->resize(40, 40);
m_buttons[i*5 + j]->move(10+ (10 + 40)*j, 50 + (10 + 40)*i);
m_buttons[i*5 +j]->setText(btnText[i*5 + j]);
connect(m_buttons[i*5 + j],SIGNAL(clicked()), this, SLOT(onButtonClicked()));
}
else
{
ret = false;
}
}
}
return ret;
}
QCalculatorUI* QCalculatorUI::NewInstance()
{
QCalculatorUI* ret = new QCalculatorUI();
if( (ret == NULL) || !ret->construct() )
{
delete ret;
ret = NULL;
}
return ret;
}
void QCalculatorUI::show()
{
QWidget::show();
setFixedSize(width(), height());
}
void QCalculatorUI::onButtonClicked()
{
QPushButton* btn = dynamic_cast<QPushButton*>(sender());
if( btn != NULL )
{
QString clickText = btn->text();
if( clickText == "<-" )
{
QString text = m_edit->text();
if( text.length() > 0 )
{
text.remove(text.length()-1,1);
m_edit->setText(text);
}
}
else if( clickText == "C" )
{
m_edit->setText("");
}
else if( clickText == "=" )
{
if( m_cal != NULL ) //绑定好借口的情况下才调用业务逻辑模块计算
{
m_cal->expression(m_edit->text()); //多态发生(m_cal->expression()),将调用m_cal实际指向的类对象的虚函数
m_edit->setText(m_cal->result()); //多态发生(m_cal->result()),将调用m_cal实际指向的类对象的虚函数
}
}
else
{
m_edit->setText(m_edit->text() + clickText);
}
}
}
voidQCalculatorUI::setCalculator(ICalculator* cal)
{
m_cal = cal; //指定接口指针指向对象
}
ICalculator* QCalculatorUI::getCalculator()
{
return m_cal; //返回使用的接口(即返回实际指向对象的地址)
}
QCalculatorUI::~QCalculatorUI()
{
}
头文件:
#ifndef _QCALCULATORUI_H_
#define _QCALCULATORUI_H_
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include "ICalculator.h"
class QCalculatorUI : public QWidget
{
Q_OBJECT
private:
QLineEdit* m_edit;
QPushButton* m_buttons[20];
ICalculator* m_cal; //指向接口类(纯虚类不能定义对象,但能定义指向纯虚类对象的指针)的指针
QCalculatorUI();
bool construct();
private slots:
void onButtonClicked();
public:
static QCalculatorUI* NewInstance();
void show();
void setCalculator(ICalculator* cal);
ICalculator* getCalculator();
~QCalculatorUI();
};
#endif
业务逻辑:
实现:
#include "QCalculatorDec.h"
QCalculatorDec::QCalculatorDec()
{
m_exp = "";
m_result = "";
}
QCalculatorDec::~QCalculatorDec()
{
}
bool QCalculatorDec::isDigitOrDot(QChar c)
{
return (('0' <= c) && (c <= '9')) || (c == '.');
}
bool QCalculatorDec::isSymbol(QChar c)
{
return isOperator(c) || (c == '(') || (c == ')');
}
bool QCalculatorDec::isSign(QChar c)
{
return (c == '+') || (c == '-');
}
bool QCalculatorDec::isNumber(QString s)
{
bool ret = false;
s.toDouble(&ret);
return ret;
}
bool QCalculatorDec::isOperator(QString s)
{
return (s == "+") || (s == "-") || (s =="*") || (s == "/");
}
bool QCalculatorDec::isLeft(QString s)
{
return (s == "(");
}
bool QCalculatorDec::isRight(QString s)
{
return (s == ")");
}
int QCalculatorDec::priority(QString s)
{
int ret = 0;
if( (s == "+") || (s == "-") )
{
ret = 1;
}
if( (s == "*") || (s == "/") )
{
ret = 2;
}
return ret;
}
bool QCalculatorDec::expression(constQString& exp)
{
bool ret = false;
QQueue<QString> spExp = split(exp);
QQueue<QString> postExp;
m_exp = exp;
if( transform(spExp, postExp) )
{
m_result = calculate(postExp);
ret = (m_result != "Error");
}
else
{
m_result = "Error";
}
return ret;
}
QString QCalculatorDec::result()
{
return m_result;
}
QQueue<QString>QCalculatorDec::split(const QString& exp)
{
QQueue<QString> ret;
QString num = "";
QString pre = "";
for(int i=0; i<exp.length(); i++)
{
if( isDigitOrDot(exp[i]) )
{
num += exp[i];
pre = exp[i];
}
else if( isSymbol(exp[i]) )
{
if( !num.isEmpty() )
{
ret.enqueue(num);
num.clear();
}
if( isSign(exp[i]) && ((pre == "") || (pre =="(") || isOperator(pre)) )
{
num += exp[i];
}
else
{
ret.enqueue(exp[i]);
}
pre = exp[i];
}
}
if( !num.isEmpty() )
{
ret.enqueue(num);
}
return ret;
}
boolQCalculatorDec::match(QQueue<QString>& exp)
{
bool ret = true;
int len = exp.length();
QStack<QString> stack;
for(int i=0; i<len; i++)
{
if( isLeft(exp[i]) )
{
stack.push(exp[i]);
}
else if( isRight(exp[i]) )
{
if( !stack.isEmpty() && isLeft(stack.top()) )
{
stack.pop();
}
else
{
ret = false;
break;
}
}
}
return ret && stack.isEmpty();
}
boolQCalculatorDec::transform(QQueue<QString>& exp,QQueue<QString>& output)
{
bool ret = match(exp);
QStack<QString> stack;
output.clear();
while( ret && !exp.isEmpty() )
{
QString e = exp.dequeue();
if( isNumber(e) )
{
output.enqueue(e);
}
else if( isOperator(e) )
{
while( !stack.isEmpty() && (priority(e) <=priority(stack.top())) )
{
output.enqueue(stack.pop());
}
stack.push(e);
}
else if( isLeft(e) )
{
stack.push(e);
}
else if( isRight(e) )
{
while( !stack.isEmpty() && !isLeft(stack.top()) )
{
output.enqueue(stack.pop());
}
if( !stack.isEmpty() )
{
stack.pop();
}
}
else
{
ret = false;
}
}
while( !stack.isEmpty() )
{
output.enqueue(stack.pop());
}
if( !ret )
{
output.clear();
}
return ret;
}
QString QCalculatorDec::calculate(QStringl, QString op, QString r)
{
QString ret = "Error";
if( isNumber(l) && isNumber(r) )
{
double lp = l.toDouble();
double rp = r.toDouble();
if( op == "+" )
{
ret.sprintf("%f", lp + rp);
}
else if( op == "-" )
{
ret.sprintf("%f", lp - rp);
}
else if( op == "*" )
{
ret.sprintf("%f", lp * rp);
}
else if( op == "/" )
{
const double P = 0.000000000000001;
if( (-P < rp) && (rp < P) )
{
ret = "Error";
}
else
{
ret.sprintf("%f", lp/ rp);
}
}
else
{
ret = "Error";
}
}
return ret;
}
QStringQCalculatorDec::calculate(QQueue<QString>& exp)
{
QString ret = "Error";
QStack<QString> stack;
while( !exp.isEmpty() )
{
QString e = exp.dequeue();
if( isNumber(e) )
{
stack.push(e);
}
else if( isOperator(e) )
{
QString rp = !stack.isEmpty() ? stack.pop() : "";
QString lp = !stack.isEmpty() ? stack.pop() : "";
QString result = calculate(lp, e, rp);
if( result != "Error" )
{
stack.push(result);
}
else
{
break;
}
}
else
{
break;
}
}
if( exp.isEmpty() && (stack.size() == 1) &&isNumber(stack.top()) )
{
ret = stack.pop();
}
return ret;
}
头文件:
#ifndef _CALCULATORCORE_H_
#define _CALCULATORCORE_H_
#include <QString>
#include <QStack>
#include <QQueue>
#include "ICalculator.h"
class QCalculatorDec : publicICalculator //ICalculator为接口类(纯虚类)
{
protected:
QString m_exp;
QString m_result;
bool isDigitOrDot(QChar c);
bool isSymbol(QChar c);
bool isSign(QChar c);
bool isNumber(QString s);
bool isOperator(QString s);
bool isLeft(QString s);
bool isRight(QString s);
int priority(QString s);
bool match(QQueue<QString>& exp);
QString calculate(QQueue<QString>& exp);
QString calculate(QString l, QString op, QString r);
bool transform(QQueue<QString>& exp,QQueue<QString>& output);
QQueue<QString> split(const QString& exp);
public:
QCalculatorDec();
~QCalculatorDec();
bool expression(const QString& exp); //重写接口类中继承的纯虚函数,满足多态条件之一
QString result(); //重写接口类中继承的纯虚函数,满足多态条件之一
};
#endif
接口:
#ifndef _ICALCULATOR_H_
#define _ICALCULATOR_H_
#include <QString>
//接口类:类中没有声明任何成员变量,所有成员函数都是公有的,所有成员函数都是纯虚函数,接口类用于实现多态
class ICalculator
{
public:
//被子类重写的成员函数expression将会变成虚函数,使用指向接口类(ICalculator)的指针指向接口类子对象且调用虚函数时发生多态,即实际调用的成员函数由接口类指针指向的对象而定
virtual bool expression(const QString& exp) = 0;
virtual QString result() = 0;
};
#endif
封装类:
实现:
#include "QCalculator.h"
QCalculator::QCalculator()
{
}
bool QCalculator::construct()
{
m_ui = QCalculatorUI::NewInstance();
if( m_ui != NULL )
{
m_ui->setCalculator(&m_cal); //使用接口,此时指向接口类的指针指向了接口类的子类,通过接口类指针调用虚函数时,将发生多态
}
return (m_ui != NULL);
}
QCalculator* QCalculator::NewInstance()
{
QCalculator* ret = new QCalculator();
if( (ret == NULL) || !ret->construct() )
{
delete ret;
ret = NULL;
}
return ret;
}
void QCalculator::show()
{
m_ui->show();
}
QCalculator::~QCalculator()
{
delete m_ui;
}
头文件:
#ifndef _QCALCULATOR_H_
#define _QCALCULATOR_H_
#include "QCalculatorUI.h"
#include "QCalculatorDec.h"
class QCalculator //封装类,封装界面与核心算法
{
protected:
QCalculatorUI* m_ui; //成员指向UI对象的指针
QCalculatorDec m_cal; //成员业务逻辑(QCalculatorDec)对象,QCalculatorDec继承自接口类
QCalculator();
bool construct();
public:
static QCalculator* NewInstance();
void show();
~QCalculator();
};
#endif // QCALCULATOR_H
主函数:
#include <QtGui/QApplication>
#include "QCalculator.h" //包含封装类,类中封装了用户界面模块与业务逻辑模块
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QCalculator* cal = QCalculator::NewInstance();
int ret = -1;
if( cal != NULL )
{
cal->show();
ret = a.exec();
delete cal;
}
return ret;
}
声明:
此文根据 狄泰学院唐老师的《QT实验分析教程》创作