軟件工程基礎結隊項目——四則運算器生成及擴展
1.Github項目鏈接及隊友博客鏈接
GitHub項目鏈接:
https://github.com/feimo49/four-operations
隊友博客鏈接:https://blog.csdn.net/qq_37745978/article/details/86308941
2.填寫表格中的預計時間
PSP2.1 | Personal Software Process Stages | 預估耗時 (分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 30 | |
Estimate | 估計這個任務需要多少時間 | 30 | |
Development | 開發 | 1930 | |
Analysis | 需求分析(包括學習新技術) | 90 | |
Design Spec | 生成設計文檔 | 60 | |
Design Review | 設計複審(和同事審覈設計文檔) | 30 | |
Coding Standard | 代碼規範(爲目前的開發制定合適的規範) | 30 | |
Design | 具體設計 | 120 | |
Coding | 具體編碼 | 1200 | |
Code Review | 代碼複審 | 60 | |
Test | 測試(自我測試,修改代碼,提交修改) | 240 | |
Reporting | 報告 | 190 | |
Test Report | 測試報告 | 120 | |
Size Measurement | 計算工作量 | 10 | |
Postmortem & Process Improvement Plan | 事後總結,並提出過程改進計劃 | 60 | |
Total | 合計 | 2150 |
3.解題思路描述
本次項目的核心算法分爲生成四則運算題目和求解答案兩個部分。下面分別對這兩個部分的設計思路進行描述。
生成題目模塊
生成題目模塊包括生成隨機的算式長度、生成隨機的運算符以及生成隨機個數的運算數。在算式生成之後,還需要進行括號匹配的檢測以及對算式格式的調整。
其具體流程爲首先生成一個隨機的算式長度,其長度不超過10。之後逐個字符地生成該算式。噹噹前字符不是算式的最後一個字符時,生成一個隨機數存入當前字符數組中,並在生成算式的過程中調整其格式,填入隨機生成運算符,同時檢測並避免乘方個數過多、乘方後數字過大以及分數分母爲0等缺陷情況,同時,爲方便同時對整數以及分數進行運算,在程序中設計num類同時表示兩種數據類型,設計symbol變量對其進行標記區分,並在該類中設計對分數的化簡機制;噹噹前字符是算式的最後一個字符時,主要流程與前述過程相同,增加了強制匹配右括號的情況。
求解題目模塊
在求解題目模塊中,主要的實現思想爲“堆棧”。將表達式轉換爲後綴表達式後,通過判斷將每一個字符存入操作數棧或操作符棧,存入後運算時分別按規則彈出兩個棧中的字符即可。
運算符的優先級定義如下:(注:優先級數字越小優先級越高)
\ | 運算符優先級 |
---|---|
乘方、左括號 | 1 |
乘號、除號 | 2 |
加號、減號 | 3 |
右括號 | 4 |
在實際運算中,爲隨機生成方便起見,將所有的運算符對應於整型數字,其中101-104分別代表+,-,*,/,105爲乘方,106爲左括號,107爲右括號。將算符、算數均存入堆棧後,直至操作符棧爲空時,操作數棧的棧頂數字即爲求解結果。
4.設計實現過程
需求分析
四則運算器的生成項目具體的需求可以分爲如下三個階段
-
第1階段
一次性生成1000個不重複的四則運算式至txt文本文件中,通過命令行:可執行文件名.exe -i n(n爲需要生成的題目數量)實現。
例:four_op.exe -i 1000
生成的四則運算式中最多有10個運算符,括號的數量不作限制。同時,除了整數之外,還要支持真分數的操作。在用戶給出當前隨機生成算式的答案後,程序能給出正誤判斷,並在用戶完成全部題目之後給出其錯誤率統計。 -
第2階段
增加可以支持乘方運算的運算符,其中乘方運算符的優先級最高,並且可以有兩種表示方法:^/ **,用戶可以通過在生成算式前的模式選擇決定乘方算符的表示方式。 -
第3階段
爲程序設計一個基於Windows窗體程序的GUI界面,增加“倒計時”使得每個題目必須在20秒內完成,如果完不成則計0分進入下一題;同時增加歷史記錄功能記錄用戶答過的題及其正確答案。
在該階段中本項目使用C#語言進行實現。
程序流程圖說明
類和模塊說明
-
Main.cpp
Main模塊爲該項目的主模塊,用戶在該模塊中進行輸入命令解析、參數設定以及模式選擇,同時對於不合法的輸入進行報錯,並實現與輸出模塊之間的接口。 -
GenerateExp.cpp
GenerateExp模塊爲隨機生成表達式模塊。包含函數BuildExp()和PrintExp(),其中BuildExp()函數隨機生成四則運算題目,將其對應整型符號存入整型數組中;PrintExp()函數將整型算式表達轉換爲字符型存入字符型數組中並將算式打印,同時返回字符型數組至主函數進行文件輸出。 -
Num類
Num類模塊中定義了 分子、分母、最大公約數、標記符號Symbol以及化簡標誌。同時在該模塊中實現了各類算符的重載。 -
Solver.cpp
Solver模塊爲求解四則運算題目的模塊,其主要函數爲get_ans()函數,實現對算式操作數、操作符的入棧、出棧操作,以根據運算規則實現對算式的求解。 -
Judge.cpp
Judge模塊爲判斷正誤模塊。該模塊的主要函數爲judge()函數和Check()函數,其中judge()函數實現用戶輸入答案與正確答案的比對並將正誤信息反饋給用戶,Check()函數用作檢驗用戶輸入答案的格式是否正確。
函數流程圖說明
5.單元測試
- 單元測試用例設計如下
- 輸入測試主要測試程序的合法輸入以及不合法輸入情況,保證程序的安全性,其設計如下:
編號 | 輸入格式 | 預期輸出 |
---|---|---|
1 | -i 10 | 正常處理,隨機生成10個算式 |
2 | Please input TWO parameters! | |
3 | -i | Please input TWO parameters |
4 | -c 5 | Please input in the correct form! |
5 | -i abc | Please input a NUMBER! |
- 算式運算符測試,檢測四則運算器中每一種算符的運算正確性,該層檢測正確纔可以進行進一步的程序分析,其中對於^運算符分別進行正常整型運算、冪指數爲0以及底數爲分數三種情況討論,其設計如下:
編號 | 操作數1 | 操作數2 | 運算符 | 預期結果 |
---|---|---|---|---|
1 | 1 | 1/2 | + | 3/2 |
2 | 1 | 1/2 | - | 1/2 |
3 | 3 | 1/2 | * | 3/2 |
4 | 3 | 1/2 | / | 6 |
5 | 3 | 2 | ^ | 9 |
6 | 3 | 0 | ^ | 1 |
7 | 1/2 | 1 | ^ | 1/2 |
- 題目查重測試
在四則運算中,由於存在加法交換律、乘法交換律以及左右結合律,故算式之間存在形式不同但邏輯運算相同的情況,在本項目中需要對該類情況進行測試,其設計如下:
編號 | 算式1 | 算式2 | 預期結果 |
---|---|---|---|
1 | 1+2+3 | 3+(1+2) | 重複 |
2 | 1+2+3 | 3+2+1 | 不重複 |
3 | 3 * 4 | 4 * 3 | 重複 |
4 | 1 * 2 * 3 | 3 * 2 * 1 | 不重複 |
5 | (1+2)* 3 | 3 *(1+2) | 不重複 |
6 | 1 ** 2 ** 3 | 3 ** 2 ** 1 | 不重複 |
7 | (1+2)*(3+4) | (3+4)*(1+2) | 不重複 |
8 | (2-1)/(5-3) | (5-3)/(2-1) | 不重複 |
9 | (3+6)/(5-3) | (6+3)/(5-3) | 重複 |
10 | (1/2+2/3)+3/4 | 1/2+(2/3+3/4) | 重複 |
11 | (1/2+2/3)* 3/4 | 3/4 *(1/2+2/3) | 重複 |
6.程序性能分析及改進
在性能分析階段,由於在診斷過程中需要不斷與用戶進行交互,故其診斷會話時間較長,達到了1分鐘20秒。
由分析報告可見,在程序中主函數main()佔用的CPU比例最大,其主要原因是在主函數中進行了文件的打開和讀操作,佔用CPU比例較大;將用戶輸入答案與正確結果進行比較的judge()函數其次,在其中進行對於用戶輸入答案的比對與反饋工作,需要與用戶界面進行交互;打印當前生成的算式的PrintExp()函數再次之,其實現打印算式的同時還需要將生成的算式寫入文件ques.txt中,需要消耗較多的CPU資源。這三個函數爲項目中消耗CPU最多的三個主要函數。
7.代碼說明
主函數說明
在主函數中實現了輸入的解析與判斷、參數設定以及主要的生成、求解、輸出接口設計等,是該項目最爲核心的函數:
int main(int argc, char * argv[])
{
if (argc < 3)
{
printf("Please input TWO parameters!\n");
system("pause");
return 0;
}
if (!strcmp(argv[1], "-i"))
{
srand((unsigned)time(NULL));
int n = atoi(argv[2]);
cout << "您希望使用哪種方式表示乘方?(輸入1選擇模式1,輸入2選擇模式2)mode-1:^/mode-2:**" << endl;
int m;
cin >>m;
getchar();
ofstream OutputFile("ques.txt");
int ac = 0;
for (int i = 0; i < n; i++)
{
char *t;
num useranswer;
int * save = BuildExp(3);
t = PrintExp(m);
OutputFile << t;
if(judge(get_ans(save)))
ac++;
}
printf("本輪題目正確率:%d/%d\n", ac, n);
}
if (strcmp(argv[1], "-i")) //輸入格式不合理的情況
{
printf("Please input in the correct form!\n");
system("pause");
return 0;
}
int flag = 0;
for (int i = 0; i < strlen(argv[2]);i++)
{
if (argv[2][i]<'0' || argv[2][i]>'9')
flag = 1;
}
if (flag == 1) //輸入非數字的情況
printf("Please input a NUMBER!\n");
system("pause");
return 0;
}
Num類
Num類定義了算式中數字的數據類型表示:
class num
{
private:
int numerator; //分子
int denominator; //分母
int gcd; //最大公約數
int symbol; //運算符
int flag; //設置化簡標誌,防止重複化簡
void get_gcd(int x, int y) //求最大公約數
{
if (y == 0)
gcd = x;
else
get_gcd(y, x%y);
}
void reduction() //化簡
{
if (numerator != 0) //分子不爲0
{
symbol = symbol * (numerator / abs(numerator))*(denominator / abs(denominator));
numerator = abs(numerator);
denominator = abs(denominator);
get_gcd(numerator, denominator);
}
else //分子爲0
{
denominator = 1;
gcd = 1;
symbol = 1;
}
flag = 1;
}
public:
num();
num(int x);
num(int x, int y, int sign);
void print();
void print(char * formula, ofstream & outtofile);
friend num operator +(num &a, num &b);
friend num operator -(num &a, num &b);
friend num operator *(num &a, num &b);
friend num operator /(num &a, num &b);
friend num operator ^(num &a, num &b); //保證b的分母爲1
friend int operator == (num &a, num &b);
};
隨機生成算式
隨機生成指定個數的四則運算式代碼如下:
//隨機化設計
default_random_engine generator(time(NULL));
normal_distribution<double> lendis(5, 3);
normal_distribution<double> numdis(5, 2);
auto lendice = bind(lendis, generator);
auto numdice = bind(numdis, generator);
int Exp[50];
int p = 0;
//mode=1 基礎,mode=2 包含分數,mode=3,包含乘方。
//隨機生成式子長度
int RandExpLen()
{
int randnum = lround(lendice());
if (randnum < 2)
randnum = 2;
else if (randnum > 10)
randnum = 10;
return randnum;
}
//隨機生成算符
int RandSymbol(int mode)
{
int randnum;
if (mode == 1)
randnum = rand() % 4 + 101;
else if (mode == 2)
randnum = rand() % 4 + 101;
else if (mode == 3)
randnum = rand() % 5 + 101;
else if (mode == 4)
randnum = rand() % 2;
return randnum;
}
//隨機生成式子中數字個數
int RandExpNum(int maxnum)
{
int randnum = lround(numdice());
if (randnum < 0)
randnum = 0;
else if (randnum > maxnum)
randnum = maxnum;
return randnum;
}
//隨機生成一個1~3的數字
int GetEasy()
{
return rand() % 3 + 1;
}
//生成算式
int* BuildExp(int mode)
{
memset(Exp, 0, sizeof(Exp));
bool HavePow = false;
int expnum = RandExpLen();
int lastbracket = 0;
p = 0;
for (int j = 1; j <= expnum; j++)
{
if (j == expnum)//最後一個數字的判斷
{
Exp[p++] = RandExpNum(10);
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy(); //返回一個1~3的數
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判斷分母0
Exp[p - 1] = 1;
if (lastbracket != 0)//若有未匹配左括號,則最後一位強制添加右括號
Exp[p++] = 107;
break;
}
else
{
Exp[p++] = RandExpNum(10);//生成隨機數
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy();
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判斷分母0
Exp[p - 1] = 1;
if (RandSymbol(4) && lastbracket > 2)//右括號
{
Exp[p++] = 107;
lastbracket = 0;
}
Exp[p++] = RandSymbol(mode);//生成隨機符號
{//檢查乘方個數
if (Exp[p - 1] == 105 && HavePow)
Exp[p - 1] = RandSymbol(1);
else if (Exp[p - 1] == 105)
HavePow = true;
}
if (RandSymbol(4) && j < expnum - 1 && lastbracket == 0 && Exp[p - 1] < 104)//左括號
{
Exp[p++] = 106;
lastbracket = 1;
}
}
if (lastbracket != 0)
lastbracket++;
}
return Exp;
}
求解四則運算式
求解算式應用堆棧方法進行算式答案的求解,具體說明在設計實現過程的類與模塊說明中進行描述:
extern int p;
//運算符和可處理十進制數之間的轉換
num cal(num n1, num n2, int opera)
{
if (opera == 101)
return n1 + n2;
else if (opera == 102)
return n1 - n2;
else if (opera == 103)
return n1 * n2;
else if (opera == 104)
return n1 / n2;
else if (opera == 105)
return n1 ^ n2;
}
//將四則運算映射到一串十進制數,0-100爲運算數
//其中101-104分別代表+,-,*,/,105爲乘方,106爲左括號,107爲右括號
num get_ans(int * operation)
{
stack <int> operators;
stack <num> operand;
for (int i = 0; i < p; i++)
{
if (operation[i] >= 0 && operation[i] <= 100)
{
num temp(operation[i]);
operand.push(temp);
}
else if (operation[i] == 105 || operation[i] == 106) //左括號與乘方必定入棧
operators.push(operation[i]);
else if (operation[i] == 103 || operation[i] == 104) //乘除會彈出乘方與乘除
{
while (!operators.empty() && (operators.top() == 103 || operators.top() == 104 || operators.top() == 105))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 101 || operation[i] == 102) //加減可能彈出乘除與乘方
{
while (!operators.empty() && (operators.top() != 106 && operators.top() != 107))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 107) //右括號會一直彈出直至左括號
{
while (operators.top() != 106)
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.pop();
}
}
while (!operators.empty())
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
return operand.top();
}
8.程序擴展:GUI界面生成
GUI界面設計顯示
在該項目中,我們小組選擇了第一個擴展方向,即使用C#語言生成一個基於Windows窗體程序的GUI界面。
主界面呈現:
在主界面中點擊START按鈕進入答題界面,該按鈕實現了倒計時、成績記錄、題目以及答題文本框顯示等功能,同時進行算式的居中設計。
答題界面呈現:
在答題界面中,左上角實現倒計時功能,每一道題有20s的答題時間,右上角實現成績記錄功能,每答對一道題,Grade++,否則Grade數值不變。在文本框中輸入當前顯示算式的答案,點擊SUBMIT按鈕進行提交。如果回答正確,則彈出消息框 “Bingo!” 如下:
如果回答錯誤,則彈出消息框 “Wrong!” ,並在下方顯示該道題目的正確答案,如下:
如果答題時間超過了20秒,則彈出超時消息框 “Time Up!” ,並重新開始下一道題:
在答題過程中,隨時可以點擊QUIT按鈕退出應用程序,也可以點擊HISTORY按鈕查看歷史記錄,得以看到自己答過的題目及其正確答案,以及當前的正確率統計。
在歷史記錄窗體中可以點擊BACK按鈕返回答題界面。
GUI界面代碼設計
使用Windows窗體項目實現GUI界面的設計。首先將之前撰寫的C++項目代碼轉換爲C#語言在該項目中進行重寫。在重寫過程中需要考慮語法的修改以及GUI中添加的相應功能的正確表述。
在將C++項目中的功能進行成功移接後,進行其與窗體界面的結合,主要在START按鈕和SUBMIT按鈕中實現窗體程序的功能。
START按鈕實現如下:
private void Start_Click(object sender, EventArgs e)
{
if (IsFirstRound)
{
Ans.Visible = true; //點擊開始答題按鈕後顯示答題文本框
Start.Visible = false;
label2.Visible = false;
Submit.Visible = true;
History.Visible = true;
ques.Visible = true;
Timer.Visible = true;
ggrade.Visible = true;
Quit.Visible = true;
ggrade.Text = "Grade: " + grade.ToString();
timer1.Start();
}
Ans.Focus();
for (int i = 0; i < 1; i++)
{
save = Generate.BuildExp(3);
Generate.PrintExp();
cnt++;
ques.Text = Generate.strsave;
int len = ques.Width;
int flen = this.Width;
int x = (flen - len) / 2;
int y = 100;
ques.Location = new Point(x, y);
}
}
SUBMIT按鈕實現如下:
private void Submit_Click(object sender, EventArgs e)
{
if(Ans.Text=="")
{
Ans.Focus();
return;
}
Judge ans = new Judge();
Num correct_ans = solve.get_ans(save, Generate.p);
int ansflag = ans.judge(correct_ans, this.Ans.Text);
correct_ans_str = correct_ans.c_Tostring();
timu = Generate.C_Tostring();
f3.History_Add(timu, correct_ans_str);
if (ansflag==1)
{
timer1.Stop();
MessageBox.Show("Bingo!");
timer1.Start();
grade+=1;
correct_cnt++;
ggrade.Text = "Grade: "+grade.ToString();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else if(ansflag==0)
{
timer1.Stop();
MessageBox.Show("Wrong!\n" + "Correct Answer:" + correct_ans_str);
timer1.Start();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else
{
timer1.Stop();
MessageBox.Show("Error:Please input the correct form!");
timer1.Start();
this.Ans.Text = "";
this.Ans.Focus();
}
f3.Correct_Rate(correct_cnt, cnt-1);
}
HISTORY按鈕實現如下:
//打開歷史記錄窗口
private void History_Click(object sender, EventArgs e)
{
f3.Show();
}
//每次生成題目時通過f3.History_Add(timu, correct_ans_str),將題目和正確答案傳入f3
public void History_Add(String Text1,String Text2)
{
record.Text += Text1 + "=" + Text2+"\r\n";
}
public void Correct_Rate(int c_cnt, int cnt)
{
correct_rate.Text = "Correct Rate : " + c_cnt + "/" + cnt;
}
9.填寫表格中的實際時間
PSP2.1 | Personal Software Process Stages | 預估耗時 (分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 30 | 35 |
Estimate | 估計這個任務需要多少時間 | 30 | 35 |
Development | 開發 | 1930 | 2005 |
Analysis | 需求分析(包括學習新技術) | 90 | 60 |
Design Spec | 生成設計文檔 | 60 | 60 |
Design Review | 設計複審(和同事審覈設計文檔) | 30 | 30 |
Coding Standard | 代碼規範(爲目前的開發制定合適的規範) | 30 | 15 |
Design | 具體設計 | 120 | 120 |
Coding | 具體編碼 | 1200 | 1345 |
Code Review | 代碼複審 | 60 | 75 |
Test | 測試(自我測試,修改代碼,提交修改) | 240 | 300 |
Reporting | 報告 | 190 | 175 |
Test Report | 測試報告 | 120 | 100 |
Size Measurement | 計算工作量 | 10 | 15 |
Postmortem & Process Improvement Plan | 事後總結,並提出過程改進計劃 | 60 | 60 |
Total | 合計 | 2150 | 2215 |
10. 實驗總結
在本次進行結對項目的完成中,我的收穫很多。我掌握了隨機生成正確的四則運算式、求解四則運算式以及設計實現完善的GUI界面的方法。同時,我意識到了與隊友協作的重要性。之前在進行個人項目的完成時不涉及合作的問題,但是在結對項目中如何與自己的隊友進行分工合作顯得至關重要。我的隊友與我是很熟悉的朋友,因此我們這次的合作十分順利~
除此之外,在GUI界面設計實現的過程中,我感覺到作爲計算機專業的學生,也需要培養自己的設計思想與審美意識,不能只會實現後端的功能而忽視前端界面的美觀設計。這次我們在如何設計美觀的界面上花了很多的時間,做出了我們認爲較爲美觀的操作界面,希望我們今後可以在這方面有所提高。