软件工程结队项目:基于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[])
主函数的流程图为:
以上函数的关系:
1. 主函数 (main)
- 负责程序的入口,处理命令行参数,决定接下来的操作。
2. 参数处理
- 检查命令行参数的数量和有效性。
- 提取参数用于后续处理。
3. 条件判断
- 根据提取的参数判断执行的操作:
- generateQuestion(): 当参数为 -n 和 -r 时被调用。
- 负责生成问题,使用 questionNum 和 maxRange。
- checkAns(): 当参数为 -e 和 -a 时被调用。
- 负责检查答案,使用 exerciseFile 和 answerFile。
- generateQuestion(): 当参数为 -n 和 -r 时被调用。
4. 错误处理
- 当参数数量不正确或选项无效时,会输出错误信息并调用 printUsage()。
- printUsage() 函数用于显示程序的使用说明,帮助用户理解如何正确输入参数。
5. 输出
- 在成功完成任务之后,都会输出 "Finish" 表示处理完毕。
五. 性能分析
在VS自带的性能分析软件中,我们可得到以下结果:
各函数详细所需时间:
同时我们也可以得到该程序的CPU使用率分布图:
由此我们可以得出,占用相关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;
}
}
七. 测试运行
可执行程序在命令行输入必要参数并执行,所得到的结果如下图所示:
以上是其中一次测试,我们还对程序进行了二十次以上的不同数据的测试,发现程序给出的答案与我们手动计算的答案相同,所以我们经过多次验证后确定程序没有问题。
八. 项目小结
1.感受
在本次组队项目中,我们考虑到了两个人擅长领域的不同,所以我们经过商量以及计划对本次项目进行分工合作,让工作效率更快更好,也能让双方都得到较为不错的体验。
2. 收获
在这次合作中,我们成功完成了项目目标,具体包括提高了工作效率和达成了预定的成果。这次经历让我在沟通和时间管理上有了显著提升,而对方在问题解决方面表现出色。我们通过分享经验,互相学习,极大丰富了彼此的视野。同时,这种紧密的团队协作让我们在面对挑战时能够迅速找到解决方案。展望未来,我希望能继续与对方合作,共同迎接新的挑战,实现更大的目标。