软件工程结队项目:基于C++实现的自动生成小学四则运算的命令行程序

这个作业属于哪个课程 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. 收获

在这次合作中,我们成功完成了项目目标,具体包括提高了工作效率和达成了预定的成果。这次经历让我在沟通和时间管理上有了显著提升,而对方在问题解决方面表现出色。我们通过分享经验,互相学习,极大丰富了彼此的视野。同时,这种紧密的团队协作让我们在面对挑战时能够迅速找到解决方案。展望未来,我希望能继续与对方合作,共同迎接新的挑战,实现更大的目标。

posted @ 2024-09-27 15:44  ra1n1  阅读(37)  评论(0编辑  收藏  举报