2020/06/02:被问及代码中//#include<system>cout << defaultfloat的事情,故过来更新一下。由于码代码的时候是半夜,不太清醒,system头文件应该是笔误,因为写到要调用system()函数,然后想引用cstdlib,结果写成system去了,然后迷糊糊地就注释着没删;defaultfloatC++11的新特性,如果编译器不支持需要手动更新编译器的C++版本或在设置中开启C++11特性的支持。

一、实验内容、实验任务和目的

根据题目要求设计一款具有以下功能的软件系统:

  1. 进行加、减、乘、除运算的练习,能分别进行加(单纯进行加法练习)、减(单纯进行减法练习)、乘(单纯进行乘法练习)、除(单纯进行除法练习)及混合(既有加,又有减,还有乘,也有除,即在一个题中只出现一种运算)运算练习,每组10题,每题10分,并给出总分;
  2. 能进行四则混合运算练习,输入数学式,能输出计算结果;
  3. 具有个税计算器功能,能计算出工资扣税信息;
  4. 具有二级及以上菜单。

二、实验步骤、过程、程序的数据结构和主要算法说明或流程

本次实验主要难度在四则混合运算练习,故决定将此项留在最后写,先将其他功能模块完成。

加减乘除运算练习的主要难度在于如何生成合适的随机数,使得算式在口算难度上具有较好的练习效果,故采取如下处理方法:

  1. 对于加法练习,生成一个较小数作为运算数,一个较大数作为结果,防止答案超过100,再反向计算出另一个运算数。
  2. 对于减法练习,生成一个较大数作为被减数,一个较小数作为减数,防止答案出现负数。
  3. 对于乘法练习,生成一个较小数作为运算数,一个较大数作为预设结果,防止答案超过100,再反向计算出另一个运算数,最后将两个运算数重新相乘即为实际结果。
  4. 对于除法练习,生成一个较小数作为除数,一个较大数作为预设被除数,防止答案小于1,再计算出结果,最后根据结果重新乘上除数即可得到实际被除数。
  5. 由于乘除法会出现小数,故都采取预设数的方法,先求出不大于预设第三数的整数作为第三数,再反向计算即可得到一个整数作为替换预设数的实际数。
  6. 由于乘除法在使用此逆向运算的方法生成随机数时容易出现乘数/除数/商为1的情况,即由于生成的两个随机数相差倍数小于2,导致实际运算出现第三数为1的情况,故将乘除法的较小数生成方式改为仅生成在[1, 25]之间的数,将较大数生产方式改为仅生成在[较小数, 100]之间的数。

由于加法与减法的生成算法、乘法与除法的生成算法之间都存在较高的相似性,故将加法与减法代码合并,将乘法与除法代码合并。

最后的混合运算练习实际上就是按1/4的概率生成一个加/减/乘/除运算,所以可以在判断到用户需求为混合运算练习时,将运算指令随机生成为[1, 4]之间的数,再进行生成式子生成即可实现。代码如下:

if(cmdcmd !=5)
    opt = cmdcmd;
else
    opt = rand() % 4 + 1;

计分系统只需判断用户输入答案是否与预期相同即可,不作详细描述。

个税计算器根据百度百科条目——个人所得税(税种)提供的计算标准,并参考新浪财经个税计算器的计算结果,即可得出所需的算法逻辑。在对于三险一金的处理上,与老师演示的有所不同,老师将三险一金不纳入工资范畴进行额外处理,但根据实际生活情况,三险一金一般已经计算在工资款项中,并在实际发放工资时进行扣除,此运算逻辑也在新浪财经的个税计算器中得以验证。

最后需要编写的是四则混合运算练习系统,由于表达式由用户输入,存在各种可能无法预料的情况,故在接收到输入时先进行表达式核验,如无意外情况才放行。

表达式的存储采用双数组的方式,将同级的运算数和运算符分开存储,方便做到一一对应。由于数组所需的长度无法预估,故采用了C++ STL中的vector数据类型作为不限长的数组。用vector的另一个原因是考虑到后续进行运算时要对数组进行删减,vector自带的erase函数可以充分满足需求。

对于表达式的求值,难点在于括号的处理和对减号用途的判断上。

对于括号问题,采用了递归求值的方式进行运算,将统一级的最外层括号内容再次调用表达式求值函数进行运算,然后返回的运算结果作为此位置的值存储,即可在最终递归结束时只留下无括号的四则混合运算表达式。

对于减号的用途判断上,根据上一次存储的数据类型进行判断即可,若上一次存储了运算数,那么这次的减号应当用作运算符;若上一次存储了运算符,那么这次的减号应当作负号计入下一次运算数。

当表达式只剩下无括号的四则混合运算时,即可从前往后运算乘除法,再将运算结果存储在参与运算的第二个运算数上,即参与运算的数为A和B,将运算结果存储在B的位置中,此存储方式是考虑到连续乘除法的情况,如A*B/C,那么第一次运算得到的表达式为A*[A*B]/C,其中A以及其后面的一个运算符已经无实际作用,[A*B]表示此次运算结果,参与下一次X/C的运算,所以下一次运算后的表达式为A*[A*B]/[A*B/C]其中第一、第二运算符和运算数都已经无实际作用。

由于此轮运算结束后,存储的运算数和运算符中有相当一部分已经不需要参与计算,故在运算过程中,将参与运算的前一位位置进行存储,方便删除。

结束此轮运算后,从后往前进行一一删除,从后往前是为了使存储的位置不失效。如果从前往后的话,删除了第一个存储的位置,那么后面的所有位置都应当减一后才是现在的数组实际位置,此方法过于复杂,故采用从后往前删除。

最后剩下来的数组存储的运算符只有加减法,直接从前往后运算至结束即可求出值。求值过程中,值的存储依旧存在参与运算的第二个运算数上。最终此轮运算结束后,运算数数组的最后一位即为表达式的答案。

最后在程序入口添加上system("color F0");system("mode con cols=60 lines=28");使窗口配色变成灰底黑字,窗口大小缩小为60列28行,方便打印。

三、程序的整体结构

由于flowchart无法正常显示,直接使用flow生成的图片

flowchart

st=>start: main
out0=>operation: 输出菜单界面
in0=>inputoutput: 输入命令
if0=>condition: <0 || >3|
if1=>condition: ==1
if2=>condition: ==2
if3=>condition: ==3
if9=>condition: ==0
sub1=>subroutine: 加/减/乘/除单项练习(100以内)
out10=>operation: 输出加减乘除二级菜单界面
in10=>inputoutput: 输入命令
if10=>condition: <0 || >5|
if11=>condition: ==1
sub11=>subroutine: 加法练习
if12=>condition: ==2
sub12=>subroutine: 减法练习
if13=>condition: ==3
sub13=>subroutine: 乘法练习
if14=>condition: ==4
sub14=>subroutine: 除法练习
if15=>condition: ==5
sub15=>subroutine: 混合练习
if19=>operation: ==0
sub2=>subroutine: 四则混合运算练习
in20=>inputoutput: 输入表达式
tFFOoA=>subroutine: tFFOoA函数
if20=>condition: 有括号
sub20=>subroutine: 运算乘除法
sub21=>subroutine: 运算加减法
out20=>operation: 输出结果
sub3=>subroutine: 个税计算器
in30=>inputoutput: 输入工资
in31=>inputoutput: 输入三险一金
sub30=>subroutine: 计算个税
out30=>operation: 输出结果
ed=>end: Return 0

st->out0->in0->if0
if0(yes)->in0
if0(no)->if1
if1(yes)->sub1->out10->in10->if10
if10(yes, right)->in10
if10(no)->if11
if11(yes, right)->sub11
if11(no)->if12
if12(yes, right)->sub12
if12(no)->if13
if13(yes, right)->sub13
if13(no)->if14
if14(yes, right)->sub14
if14(no)->if15
if15(yes, right)->sub15
if15(no)->if19
if19(right)->out0
if1(no)->if2
if2(yes)->sub2->in20->tFFOoA->if20
if20(yes, right)->tFFOoA
if20(no)->sub20->sub21->out20
out20(right)->out0
if2(no)->if3
if3(yes)->sub3->in30->in31->sub30->out30
out30(right)->out0
if3(no)->if9
if9(yes)->ed

四、程序的完整代码

#include<iostream>
#include<vector>
#include<ctime>
#include<iomanip>
//#include<cstdlib>
//#include<system>
//#include<string>
//这几个头文件在自己机器环境编译时可有可无,故注释掉,如无法编译需要取消注释
using namespace std;
const string welcome = "\n\
----小学生算术练习系统,欢迎使用!----\n\
\n\
    本系统有以下功能:\n\
    |--1.加/减/乘/除单项练习(100以内)\n\
    |--2.四则混合运算练习\n\
    |--3.个税计算器\n\
    |--0.退出系统\n\
\n\
请选择(0-4):";

const string menu1 = "\n\
----加/减/乘/除单项练习系统,欢迎使用!----\n\
\n\
    本系统有以下练习:\n\
    |--1.加法运算\n\
    |--2.减法运算\n\
    |--3.乘法运算\n\
    |--4.除法运算\n\
    |--5.混合运算\n\
    |--0.退出练习\n\
\n\
请选择(0-5):";

const string menu2 = "\n\
----四则混合运算练习系统,欢迎使用!----\n\
\n\
    本系统支持以下字符:\n\
    0123456789+-*/()\n\
\n";

const string menu3 = "\n\
----个税计算器,欢迎使用!----\n\
\n\
    计算规则参照自百度百科“个人所得税”条目\n\
    计算结果与新浪财经个税计算器【http://finance.sina.com.cn/calc/tax_pers_income.html】相同\n\
\n";
//测试用例
//-1+2*3/(13-1*2)*(7*-(3-1))*-(((3)))
//3*(6-78)+52/4+((7*9+9)/8)*9/3
//-1+2
//1-2
//1*-2
//4/-2
//-((-(-3)))
double tFFOoA(string expression){
    //cout << expression << endl;
    int len = expression.size();
    vector<double>num(0);
    vector<char>opt(0);
    string typeFlag = "num";
    int left = 0, right = 0;
    double minusSign = 1.;
    // 开始去括号
    while(right < len){
        if(typeFlag == "num"){
            if(expression[left] == '('){
                int parenthese = -1;
                while(parenthese != 0){
                    right++;
                    if(expression[right] == '(')
                        parenthese--;
                    else if(expression[right] == ')')
                        parenthese++;
                }
                num.push_back(minusSign * tFFOoA(expression.substr(left + 1, right - left - 1)));
                minusSign = 1.;
                typeFlag = "opt";
                right++;
                left = right;
            }else if(expression[left] == '-'){
                minusSign = -1.;
                left++;
                right++;
            }else if(expression[left] >= '0' && expression[left] <= '9'){
                int tmpNum = expression[left] - '0';
                right++;
                while(expression[right] >= '0' && expression[right] <= '9'){
                    tmpNum = tmpNum * 10 + expression[right] - '0';
                    right++;
                }
                num.push_back(minusSign * tmpNum);
                minusSign = 1.;
                typeFlag = "opt";
                left = right;
            }
        }else if(typeFlag == "opt"){
            opt.push_back(expression[left]);
            typeFlag = "num";
            right++;
            left = right;
        }
    }
    /*
    for(int i = 0; i < opt.size(); i++){
        cout << num[i] << ' ' << opt[i] << ' ';
    }
    cout << num.back() << endl;
    */
    // 开始计算无括号四则
    vector<int>needDelete(0);
    for(int i = 0; i < opt.size(); i++){
        if(opt[i] == '*'){
            num[i + 1] = num[i] * num[i + 1];
            needDelete.push_back(i);
        }else if(opt[i] == '/'){
            num[i + 1] = num[i] / num[i + 1];
            needDelete.push_back(i);
        }
    }
    for(int i = needDelete.size() - 1; i >= 0; i--){
        num.erase(num.begin() + needDelete[i]);
        opt.erase(opt.begin() + needDelete[i]);
    }
    for(int i = 0; i < opt.size(); i++){
        if(opt[i] == '+')
            num[i + 1] = num[i] + num[i + 1];
        else if(opt[i] == '-')
            num[i + 1] = num[i] - num[i + 1];
    }
    return num.back();
}
int main(){
    system("color F0");
    system("mode con cols=60 lines=28");
    while(true){
        cout << welcome;
        int cmd = -1;
        cin >> cmd;
        while(cmd < 0 || cmd > 3){
            cout << "功能序号无效,请重新输入:";
            cin >> cmd;
        }
        if(cmd == 1){
            system("cls");
            cout << menu1;
            int cmdcmd = -1;
            cin >> cmdcmd;
            while(cmdcmd < 0 || cmdcmd > 5){
                cout << "练习序号无效,请重新输入:";
                cin >> cmdcmd;
            }
            if(cmdcmd != 0){
                srand(time(NULL));
                system("cls");
                cout << "\n----";
                if(cmdcmd == 1)
                    cout << "加法";
                else if(cmdcmd == 2)
                    cout << "减法";
                else if(cmdcmd == 3)
                    cout << "乘法";
                else if(cmdcmd == 4)
                    cout << "除法";
                else if(cmdcmd == 5)
                    cout << "混合";
                cout << "练习系统,欢迎使用!----\n\n";
                int point = 0;
                for(int i = 1; i<= 10; i++){
                    cout << "    (" << i << ") ";
                    int as = 0;
                    int opt = 0;
                    if(cmdcmd !=5)
                        opt = cmdcmd;
                    else
                        opt = rand() % 4 + 1;
                    if(opt == 1 || opt == 2){
                        int x1 = rand() % 101; // [0, 100]
                        int x2 = rand() % 101; // [0, 100]
                        if(x1 < x2){ // 加法交换后较大数作答案,防止答案超100;减法交换防止较小数被减产生负数
                            int tmp = x1;
                            x1 = x2;
                            x2 = tmp;
                        }
                        if(opt == 1){
                            as = x1; x1 = x2; x2 = as - x1;
                            cout << x1 << " + " << x2 << " = ";
                        }else if(opt == 2){
                            as = x1 - x2;
                            cout << x1 << " - " << x2 << " = ";
                        }
                    }else if(opt == 3 || opt == 4){
                        int x1 = rand() % (26 - 1) + 1; // [1, 25] 作乘/除数
                        int x2 = rand() % (101 - x1) + x1; // [x1, 100] 乘法作近似答案,避免答案超100;除法作近似被除数
                        if(opt == 3){
                            as = x2; x2 = x1; x1 = as / x2; as = x1 * x2; // 求另一个乘数并精确答案
                            cout << x1 << " * " << x2 << " = ";
                        }else if(opt == 4){
                            as = x2 / x1; x2 = x1 * as; // 求答案并精确被除数
                            cout << x2 << " / " << x1 << " = ";
                        }
                    }
                    int inas;
                    cin >> inas;
                    if(inas == as){
                        cout << "    答案正确\n";
                        point += 10;
                    }else
                        cout << "    答案错误,正确答案是 " << as << " \n";
                }
                cout << "\n满分100分,您最终获得了 " << point << " 分!\n\n";
                system("pause");
            }
        }else if(cmd == 2){
            system("cls");
            cout << menu2;
            cout << "请输入您的想要计算的整数四则运算表达式:";
            bool needInput = true;
            string expression = "";
            while(needInput){
                cin >> expression;
                int len = expression.size();
                int parenthese = 0;
                bool charLegitimacy = true;
                for(int i = 0; i < len; i++){
                    if(expression[i] == '(')
                       parenthese++;
                    else if(expression[i] == ')')
                        parenthese--;
                    else if(!(expression[i] >= '0' && expression[i] <= '9' || expression[i] == '+' || expression[i] == '-' || expression[i] == '*' || expression[i] == '/')){
                        charLegitimacy = false;
                        break;
                    }
                }
                if(!charLegitimacy)
                    cout << "表达式存在非法字符,请检查后重新输入:";
                else if(parenthese != 0)
                    cout << "表达式括号不匹配,请检查后重新输入:";
                else
                    needInput = false;
            }
            cout << "表达式求值结果为 " << tFFOoA(expression) << endl;
            system("pause");
        }else if(cmd == 3){
            system("cls");
            cout << menu3;
            int x[3] = {-1, -1, 0};
            double as = .0;
            string tips[2] = {"请输入您的月收入", "请输入您的三险一金"};
            bool flag = true;
            for(int i = 0; i < 2; i++){
                cout << tips[i] << "(正整数)(输入其他则退出):";
                cin >> x[i];
                if(x[i] <= 0){
                    if(cin.fail()){
                        cin.clear();
                        cin.ignore(100, '\n');
                    }
                    flag = false;
                    break;
                }
            }
            if(flag){
                x[2] = x[0] - 5000 - x[1];
                if(x[2] <= 0){
                    as = .0;
                }else if(x[2] <= 3000){
                    as = 0.03 * x[2];
                }else if(x[2] <= 12000){
                    as = 0.03 * 3000 + 0.1 * (x[2] - 3000);
                }else if(x[2] <= 25000){
                    as = 0.03 * 3000 + 0.1 * (12000 - 3000) + 0.2 * (x[2] - 12000);
                }else if(x[2] <= 35000){
                    as = 0.03 * 3000 + 0.1 * (12000 - 3000) + 0.2 * (25000 - 12000) + 0.25 * (x[2] - 25000);
                }else if(x[2] <= 55000){
                    as = 0.03 * 3000 + 0.1 * (12000 - 3000) + 0.2 * (25000 - 12000) + 0.25 * (35000 - 25000) + 0.3 * (x[2] - 35000);
                }else if(x[2] <= 80000){
                    as = 0.03 * 3000 + 0.1 * (12000 - 3000) + 0.2 * (25000 - 12000) + 0.25 * (35000 - 25000) + 0.3 * (55000 - 35000) + 0.35 * (x[2] - 55000);
                }else{
                    as = 0.03 * 3000 + 0.1 * (12000 - 3000) + 0.2 * (25000 - 12000) + 0.25 * (35000 - 25000) + 0.3 * (55000 - 35000) + 0.35 * (80000 - 55000) + 0.45 * (x[2] - 80000);
                }
                cout << fixed << setprecision(2);
                cout << "\n你应缴的个人所得税为 " << as << " 元,你的实发工资为 " << x[0] - x[1] - as << " 元\n";
                cout << defaultfloat;
                system("pause");
            }
        }else if(cmd == 0){
            break;
        }
        system("cls");
    }
    cout << "感谢使用!\n";
    system("pause");
    return 0;
}

五、测试结果

所有功能皆可正常实现,未发现明显bug。
菜单界面
加减乘除二级菜单界面
加减乘除练习界面,以混合运算为例
四则混合运算界面
个税计算器界面
退出界面

六、分析与讨论

本次实验的难度较大,十分考验自身的逻辑分析能力和全局统筹思想。在编写代码和调试的过程中,出现了许多无法预料的错误。并且由于程序的四则混合运算模块存在大量的运算处理、逻辑判断和递归调用,一旦由于疏忽出现bug很难排查,所以经过此次实验,我写代码的严谨性和找bug的能力也得到了提高。