軟件工程基礎結隊項目——四則運算器生成

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#語言進行實現。

程序流程圖說明

Created with Raphaël 2.2.0開始輸入指令選擇模式生成題目求解題目輸入答案判斷對錯題目是否做完輸出作答正確率結束yesno

類和模塊說明

  • 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()函數用作檢驗用戶輸入答案的格式是否正確。

函數流程圖說明

Created with Raphaël 2.2.0開始Main()輸入是否合法模式選擇:1/2?BuildExp()函數PrintExp()函數get_ans()函數judge()函數結束輸入不合法報錯yesno

5.單元測試

  • 單元測試用例設計如下
  1. 輸入測試主要測試程序的合法輸入以及不合法輸入情況,保證程序的安全性,其設計如下:
編號 輸入格式 預期輸出
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!
  1. 算式運算符測試,檢測四則運算器中每一種算符的運算正確性,該層檢測正確纔可以進行進一步的程序分析,其中對於^運算符分別進行正常整型運算、冪指數爲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. 題目查重測試
    在四則運算中,由於存在加法交換律、乘法交換律以及左右結合律,故算式之間存在形式不同但邏輯運算相同的情況,在本項目中需要對該類情況進行測試,其設計如下:
編號 算式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界面設計實現的過程中,我感覺到作爲計算機專業的學生,也需要培養自己的設計思想與審美意識,不能只會實現後端的功能而忽視前端界面的美觀設計。這次我們在如何設計美觀的界面上花了很多的時間,做出了我們認爲較爲美觀的操作界面,希望我們今後可以在這方面有所提高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章