軟體工程結隊專案:基於C++實現的自動生成小學四則運算的命令列程式

ra1n1發表於2024-09-27
這個作業屬於哪個課程 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34
這個作業要求在哪裡 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13230
這個作業的目標 <運用C++實現四則運演算法則的命令列程式>
團隊成員1 李梓灝3122004695
團隊成員2 吳燦豪3122004710

一.Github地址

https://github.com/Memset-Lee/Memset-Lee/tree/main/3122004695-PartnerProject

二.程式需求

1.題目:實現一個自動生成小學四則運算題目的命令列程式(也可以用影像介面,具有相似功能)。
2.說明:
自然數:0, 1, 2, …。
真分數:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
運算子:+, −, ×, ÷。
括號:(, )。
等號:=。
分隔符:空格(用於四則運算子和等號前後)。
算術表示式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2為表示式,n為自然數或真分數。
四則運算題目:e = ,其中e為算術表示式。
3.需求:
使用 -n 引數控制生成題目的個數,例如
Myapp.exe -n 10
將生成10個題目。
使用 -r 引數控制題目中數值(自然數、真分數和真分數分母)的範圍,例如
Myapp.exe -r 10
將生成10以內(不包括10)的四則運算題目。該引數可以設定為1或其他自然數。該引數必須給定,否則程式報錯並給出幫助資訊。
生成的題目中計算過程不能產生負數,也就是說算術表示式中如果存在形如e1− e2的子表示式,那麼e1≥ e2。
生成的題目中如果存在形如e1÷ e2的子表示式,那麼其結果應是真分數。
每道題目中出現的運算子個數不超過3個。
程式一次執行生成的題目不能重複,即任何兩道題目不能透過有限次交換+和×左右的算術表示式變換為同一道題目。例如,23 + 45 = 和45 + 23 = 是重複的題目,6 × 8 = 和8 × 6 = 也是重複的題目。3+(2+1)和1+2+3這兩個題目是重複的,由於+是左結合的,1+2+3等價於(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重複的兩道題,因為1+2+3等價於(1+2)+3,而3+2+1等價於(3+2)+1,它們之間不能透過有限次交換變成同一個題目。
生成的題目存入執行程式的當前目錄下的Exercises.txt檔案,格式如下:
四則運算題目1
四則運算題目2
……
其中真分數在輸入輸出時採用如下格式,真分數五分之三表示為3/5,真分數二又八分之三表示為2’3/8。
在生成題目的同時,計算出所有題目的答案,並存入執行程式的當前目錄下的Answers.txt檔案,格式如下:
答案1
答案2
特別的,真分數的運算如下例所示:1/6 + 1/8 = 7/24。
程式應能支援一萬道題目的生成。
程式支援對給定的題目檔案和答案檔案,判定答案中的對錯並進行數量統計,輸入引數如下:
Myapp.exe -e .txt -a .txt
統計結果輸出到檔案Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”後面的數字5表示對/錯的題目的數量,括號內的是對/錯題目的編號。為簡單起見,假設輸入的題目都是按照順序編號的符合規範的題目。

三.PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 60 60
Estimate 估計這個任務需要多少時間 30 30
Development 開發 240 300
Analysis 需求分析(包括學習新技術 60 30
Design Review 生成設計文件 120 60
Coding Standard 設計複審(為目前的開發制定合適的規範) 30 30
Design 具體設計 60 60
Coding 具體編碼 180 210
Code Review 程式碼複審 30 60
Test 測試(自我測試,修改程式碼,提交修改) 90 90
Reporting 報告 120 60
Test Repor 測試報告 60 30
Size Measurement 計算工作量 30 30
Postmortern & Process Improvement Plan 事後總結,並提出過程改進計劃 30 30
合計 970 1050

四.程式設計分佈

程式的宏定義和全域性變數:

點選檢視程式碼
#define ll long long
#define llf LLONG_MAX
using namespace std;

ll questionNum = 100, maxRange = 100;//初始題目數量,初始自然數範圍 
ll parenthesesProbability = 5, denominatorRange = 100;//括號機率,分母範圍
string exerciseFile, answerFile;//題目檔案,答案檔案
vector<string>allSymbol = { "+","-","*","/" };//運算子
static mt19937_64 randomNumberGenerator(chrono::steady_clock::now().time_since_epoch().count());//隨機數生成器
uniform_int_distribution<ll>symbolNumRange(1, 3);//符號數量
uniform_int_distribution<ll>symbolRange(0, 3);//運算子種類
uniform_int_distribution<ll>parentheses(0, 100);//括號

程式由12個子函式組成,分別為:

1. ll gcd(ll a, ll b) //求最大公因數
2. struct Number //自然結構體
3. string addParentheses(string s)//隨機新增括號
4. Number getNum(string s, ll idx)//獲取表示式中的自然數
5. string getString(Number x)//將自然數化為正確形式
6. string getSimpleAns(string s)//計算無括號式子答案
7. bool checkParentheses(string s)//查詢括號
8. string getAns(string s)//計算答案
9. void generateQuestion()//生成問題
10. string getTrue(string s)//去除標號
11. void outputCheckAns(vectorcorrect, vectorwrong)//輸出判斷對錯並進行數量統計的結果
12. void checkAns()//判斷答案對錯並進行數量統計
13. int main(int argc, char* argv[])

主函式的流程圖為:
image

以上函式的關係:
1. 主函式 (main)
  • 負責程式的入口,處理命令列引數,決定接下來的操作。
2. 引數處理
  • 檢查命令列引數的數量和有效性。
  • 提取引數用於後續處理。
3. 條件判斷
  • 根據提取的引數判斷執行的操作:
    • generateQuestion(): 當引數為 -n 和 -r 時被呼叫。
      • 負責生成問題,使用 questionNum 和 maxRange。
    • checkAns(): 當引數為 -e 和 -a 時被呼叫。
      • 負責檢查答案,使用 exerciseFile 和 answerFile。
4. 錯誤處理
  • 當引數數量不正確或選項無效時,會輸出錯誤資訊並呼叫 printUsage()。
  • printUsage() 函式用於顯示程式的使用說明,幫助使用者理解如何正確輸入引數。
5. 輸出
  • 在成功完成任務之後,都會輸出 "Finish" 表示處理完畢。

五. 效能分析

在VS自帶的效能分析軟體中,我們可得到以下結果:
image
各函式詳細所需時間:
image
同時我們也可以得到該程式的CPU使用率分佈圖:
image
由此我們可以得出,佔用相關cpu以及耗費時間較長的程式其實大部分都是呼叫庫函式,其餘部分執行時間不算太多。但我們對自然數結構體進行了相對應的改進,降低程式出BUG的機率,同時也對部分函式進行改進,降低程式的時間複雜度。
對Number結構體的改進:

點選檢視程式碼
#include <iostream>
#include <numeric> // for std::gcd
#include <cstdlib>

using namespace std;

struct Number // 自然數結構體
{
    ll numerator, denominator; // numerator:分子,denominator:分母

    // 建構函式
    Number(ll num = 0, ll den = 1) : numerator(num), denominator(den) {
        if (denominator == 0) {
            throw invalid_argument("Denominator cannot be zero");
        }
        simplify();
    }

    // 簡化分數
    void simplify() {
        ll gcd_value = gcd(abs(numerator), abs(denominator));
        numerator /= gcd_value;
        denominator /= gcd_value;
        // 如果分母為負數,調整符號
        if (denominator < 0) {
            numerator = -numerator;
            denominator = -denominator;
        }
    }

    Number operator + (const Number& x) const {
        ll temp1 = numerator * x.denominator + x.numerator * denominator;
        ll temp2 = denominator * x.denominator;
        return {temp1, temp2}; // 返回未簡化的分數
    }

    Number operator - (const Number& x) const {
        ll temp1 = numerator * x.denominator - x.numerator * denominator;
        ll temp2 = denominator * x.denominator;
        return {temp1, temp2}; // 返回未簡化的分數
    }

    Number operator * (const Number& x) const {
        ll temp1 = numerator * x.numerator;
        ll temp2 = denominator * x.denominator;
        return {temp1, temp2}; // 返回未簡化的分數
    }

    Number operator / (const Number& x) const {
        if (x.numerator == 0) {
            throw invalid_argument("Cannot divide by zero");
        }
        ll temp1 = numerator * x.denominator;
        ll temp2 = denominator * x.numerator;
        return {temp1, temp2}; // 返回未簡化的分數
    }

    // 輸出過載
    friend ostream& operator<<(ostream& os, const Number& n) {
        os << n.numerator << "/" << n.denominator;
        return os;
    }
};

六. 程式碼說明

重要函式程式碼:struct Number //自然數結構體

程式碼分析

1. 結構體定義:
  • Number結構體包含兩個成員:numerator(分子)和denominator(分母)。
2. 運算子過載:
  • 每個運算子過載方法都按照分數的運算規則來計算結果。
  • 在每個運算結束後,使用 gcd 函式來約分結果。
3. 約分過程:
  • 使用 gcd 函式計算分子和分母的最大公約數,並進行簡化。
點選檢視程式碼
struct Number//自然數結構體
{
	ll numerator, denominator;//numerator:分子,denominator:分母
	Number operator + (const Number& x) const
	{
		ll temp1 = numerator * x.denominator + x.numerator * denominator;
		ll temp2 = denominator * x.denominator;
		return { temp1 / gcd(abs(temp1),abs(temp2)),temp2 / gcd(abs(temp1),abs(temp2)) };
	}
	Number operator - (const Number& x) const
	{
		ll temp1 = numerator * x.denominator - x.numerator * denominator;
		ll temp2 = denominator * x.denominator;
		return { temp1 / gcd(abs(temp1),abs(temp2)),temp2 / gcd(abs(temp1),abs(temp2)) };
	}
	Number operator * (const Number& x) const
	{
		ll temp1 = numerator * x.numerator;
		ll temp2 = denominator * x.denominator;
		return { temp1 / gcd(abs(temp1),abs(temp2)),temp2 / gcd(abs(temp1),abs(temp2)) };
	}
	Number operator / (const Number& x) const
	{
		ll temp1 = numerator * x.denominator;
		ll temp2 = denominator * x.numerator;
		return { temp1 / gcd(abs(temp1),abs(temp2)),temp2 / gcd(abs(temp1),abs(temp2)) };
	}
};

重要函式程式碼:string addParentheses(string s)//隨機新增括號

程式碼分析

1. 計數運算子:
  • 使用 cnt1 和 cnt2 分別計算加法/減法(+ 和 -)和乘法/除法(* 和 /)運算子的數量。
2. 查詢最後一個加法或減法運算子:
  • 透過 idx 記錄最後一次出現的加法或減法運算子的位置。
3. 查詢空格:
  • 使用 idx1 和 idx2 找到最近的空格,以便確定括號應該放置的位置。
4. 新增括號:
  • 根據找到的位置,構建新的字串,將括號新增在相應的位置。
5. 返回結果:
  • 如果沒有條件滿足,則返回原始字串。
點選檢視程式碼
string addParentheses(string s)//隨機新增括號
{
	string temp = "";
	ll i, tempidx, cnt1 = 0, cnt2 = 0, idx = -1, idx1 = -1, idx2 = -1;
	for (i = 0; i < (ll)s.size(); i++)
	{
		if (s[i] == '+' || s[i] == '-') cnt1++, idx = i;
		else if (s[i] == '*' || (s[i] == '/' && s[i - 1] == ' ' && s[i + 1] == ' ')) cnt2++;
	}
	if (cnt1 != 0 && cnt2 != 0 && parentheses(randomNumberGenerator) % parenthesesProbability == 0)
	{
		tempidx = idx - 2;
		while (tempidx >= 0)
		{
			if (s[tempidx] == ' ')
			{
				idx1 = tempidx;
				break;
			}
			tempidx--;
		}
		tempidx = idx + 2;
		while (tempidx < (ll)s.size())
		{
			if (s[tempidx] == ' ')
			{
				idx2 = tempidx;
				break;
			}
			tempidx++;
		}
		if (idx1 == -1)
		{
			temp += "(";
			for (i = 0; i < idx2; i++) temp += s[i];
			temp += ")";
			for (; i < (ll)s.size(); i++) temp += s[i];
		}
		else if (idx2 == -1)
		{
			for (i = 0; i <= idx1; i++) temp += s[i];
			temp += "(";
			for (; i < (ll)s.size(); i++) temp += s[i];
			temp += ")";
		}
		else if (idx1 != -1 && idx2 != -1)
		{
			for (i = 0; i <= idx1; i++) temp += s[i];
			temp += "(";
			for (; i < idx2; i++) temp += s[i];
			temp += ")";
			for (; i < (ll)s.size(); i++) temp += s[i];
		}
		return temp;
	}
	else
	{
		return s;
	}
}

七. 測試執行

可執行程式在命令列輸入必要引數並執行,所得到的結果如下圖所示:
image
image
image
image
image
image
以上是其中一次測試,我們還對程式進行了二十次以上的不同資料的測試,發現程式給出的答案與我們手動計算的答案相同,所以我們經過多次驗證後確定程式沒有問題。

八. 專案小結

1.感受

在本次組隊專案中,我們考慮到了兩個人擅長領域的不同,所以我們經過商量以及計劃對本次專案進行分工合作,讓工作效率更快更好,也能讓雙方都得到較為不錯的體驗。

2. 收穫

在這次合作中,我們成功完成了專案目標,具體包括提高了工作效率和達成了預定的成果。這次經歷讓我在溝通和時間管理上有了顯著提升,而對方在問題解決方面表現出色。我們透過分享經驗,互相學習,極大豐富了彼此的視野。同時,這種緊密的團隊協作讓我們在面對挑戰時能夠迅速找到解決方案。展望未來,我希望能繼續與對方合作,共同迎接新的挑戰,實現更大的目標。

相關文章