软工第三次作业--结对作业
软工第三次作业结对作业——实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)
| 项目作业 | 实现一个自动生成小学四则运算题目的命令行程序 |
|---|---|
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13470 |
| 这个作业的目标 | 提高代码能力与psp的评判 |
基本信息
项目的Github链接:https://github.com/ApplePI-xu/3123004185
项目参与成员:
| 姓名 | 学号 |
|---|---|
| 张嘉铭 | 3123004203 |
| 林旭坚 | 3123004185 |
PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 15 |
| Estimate | 估计这个任务需要多少时间 | 60 | 70 |
| Development | 开发 | 40 | 30 |
| Analysis | 需求分析(包括学习新技术) | 30 | 30 |
| Design Spec | 生成设计文档 | 10 | 5 |
| Design Review | 设计复审 | 5 | 5 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 5 | 5 |
| Design | 具体设计 | 30 | 25 |
| Coding | 具体编码 | 30 | 40 |
| Code Review | 代码复审 | 20 | 10 |
| Test | 测试(自我测试,修改代码,提交修改) | 30 | 20 |
| Reporting | 报告 | 10 | 10 |
| Test Report | 测试报告 | 10 | 10 |
| Size Measurement | 计算工作量 | 20 | 20 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 10 | 10 |
一、效能分析
性能分析图:


CPU 性能分析
测试规模: 50000道题目
总运行时间: 219.54ms
总采样时间: 80ms
CPU 最耗时的函数 (Top 5)
| 排名 | 函数 | flat时间 | flat% | cum时间 | cum% | 说明 |
|---|---|---|---|---|---|---|
| 1 | runtime.memclrNoHeapPointers |
20ms | 25% | 20ms | 25% | 内存清零操作 |
| 2 | runtime.memmove |
20ms | 25% | 20ms | 25% | 内存移动操作 |
| 3 | runtime.mallocgc |
10ms | 12.5% | 40ms | 50% | 内存分配 |
| 4 | expression.needsParenLeft |
10ms | 12.5% | 10ms | 12.5% | 括号判断逻辑 |
| 5 | math/rand.globalRand |
10ms | 12.5% | 10ms | 12.5% | 随机数生成 |
业务逻辑函数耗时
| 函数 | cum时间 | cum% | 说明 |
|---|---|---|---|
GenerateUniqueExpressions |
30ms | 37.5% | 生成不重复表达式(主函数) |
ToString |
20ms | 25% | 表达式转字符串 |
GenerateExpression |
20ms | 25% | 生成单个表达式 |
Evaluate |
10ms | 12.5% | 表达式求值 |
内存分析
总内存占用: 24280.98kB ≈ 23.7MB
测试规模: 50000道题目
占用内存最多的函数 (Top 5)
| 排名 | 函数 | 内存占用 | flat% | cum内存 | cum% | 说明 |
|---|---|---|---|---|---|---|
| 1 | expression.generateNode |
10240.47kB | 42.17% | 11776.50kB | 48.50% | 表达式节点生成 |
| 2 | runtime.allocm |
5643.01kB | 23.24% | 5643.01kB | 23.24% | 运行时内存管理 |
| 3 | fmt.Sprintf |
2048.04kB | 8.43% | 2048.04kB | 8.43% | 字符串格式化 |
| 4 | expression.GenerateUniqueExpressions |
1576.28kB | 6.49% | 16424.85kB | 67.64% | 总控函数 |
| 5 | fraction.NewFraction |
1536.04kB | 6.33% | 1536.04kB | 6.33% | 分数对象创建 |
累计内存占用 (cum)
| 函数 | cum内存 | cum% | 说明 |
|---|---|---|---|
expression.GenerateExpression |
12288.51kB | 50.61% | 单个表达式生成总开销 |
expression.GenerateUniqueExpressions |
16424.85kB | 67.64% | 整体生成流程 |
expression.Normalize |
2048.04kB | 8.43% | 表达式规范化(去重) |
结论
1. 最耗时的操作
- 内存操作占主导 (50%):
memclrNoHeapPointers+memmove+mallocgc - 业务逻辑耗时:
GenerateUniqueExpressions(37.5%) - 字符串操作:
ToString(25%)
2. 占用内存最多的函数
-
expression.generateNode: 10.24MB (42.17%) - 最大内存消耗者
-
runtime.allocm: 5.64MB (23.24%) - 运行时开销
-
fmt.Sprintf: 2MB (8.43%) - 字符串格式化开销大
3. 性能特点
优势:
- 生成速度极快: 50000题仅需220ms
- 平均速度: ~227,000题/秒
- 内存效率高: 50000题仅占用24MB
瓶颈:
- 大量小对象分配(节点、分数对象)
- 字符串操作频繁(
fmt.Sprintf) - 内存操作占CPU时间的50%
优化建议
优先级1: 减少内存分配(最大收益)
问题: generateNode 占用42%的内存
解决方案: 使用对象池复用
预期收益: 减少30-40%内存分配
优先级2: 优化字符串操作
问题: fmt.Sprintf 占用8.43%内存
解决方案: 使用 strings.Builder
预期收益: 减少字符串分配50%
优先级3: 减少分数对象创建
问题: NewFraction 创建大量对象
解决方案:
- 缓存常用分数(如0, 1)
- 考虑使用栈分配而非堆分配
预期收益: 减少10-15%内存分配
二、设计与实现的过程及其代码分析
代码主要结构
四则运算/
├── main.go # 主程序(350行)
│ ├── 命令行参数解析
│ ├── 题目生成流程
│ ├── 答案批改流程
│ └── 文件读写操作
│
├── fraction/ # 分数运算模块(240行)
│ └── fraction.go
│ ├── 分数数据结构
│ ├── 四则运算实现
│ ├── 分数化简(GCD/LCM)
│ └── 分数比较和解析
│
├── expression/ # 表达式处理模块(600行)
│ └── expression.go
│ ├── 表达式树结构
│ ├── 随机表达式生成
│ ├── 表达式求值
│ ├── 表达式规范化(去重)
│ └── 表达式解析
│
└── 文档/ # 5个文档文件
├── README.md
├── 快速入门.md
├── 项目总结.md
├── 需求检查表.md
└── 解决乱码问题.md
主函数模块
n := flag.Int("n", 0, "生成题目的个数")
r := flag.Int("r", 0, "题目中数值的范围")
exerciseFile := flag.String("e", "", "练习题文件路径")
answerFile := flag.String("a", "", "答案文件路径")
cpuProfile := flag.String("cpuprofile", "", "写入CPU性能分析到指定文件")
memProfile := flag.String("memprofile", "", "写入内存性能分析到指定文件")
生成练习题模块
func generateExercises(count, maxRange int) error {
if count <= 0 {
return fmt.Errorf("题目数量必须大于0")
}
if maxRange <= 0 {
return fmt.Errorf("数值范围必须大于0")
}
// 生成不重复的表达式
fmt.Printf("正在生成 %d 道题目...\n", count)
expressions, err := expression.GenerateUniqueExpressions(count, maxRange)
if err != nil {
return err
}
// 计算答案
exercises := make([]string, len(expressions))
answers := make([]string, len(expressions))
for i, expr := range expressions {
exercises[i] = fmt.Sprintf("%d. %s =", i+1, expr.ToString())
result, err := expr.Evaluate()
if err != nil {
return fmt.Errorf("计算第 %d 题失败: %v", i+1, err)
}
answers[i] = fmt.Sprintf("%d. %s", i+1, result.String())
}
// 写入文件
err = writeLines("Exercises.txt", exercises)
if err != nil {
return fmt.Errorf("写入题目文件失败: %v", err)
}
err = writeLines("Answers.txt", answers)
if err != nil {
return fmt.Errorf("写入答案文件失败: %v", err)
}
return nil
}
说明:
支持 -n 参数控制生成题目数量
支持 -r 参数控制数值范围
生成自然数和真分数
支持四则运算:+、-、×、÷
支持括号
每道题目运算符数量1-3个
确保计算过程不产生负数
确保除法结果为真分数或整数
自动去重(考虑交换律和结合律)
支持一万道题目的生成
题目保存到 Exercises.txt
答案保存到 Answers.txt
批改答案模块
func gradeAnswers(exerciseFile, answerFile string) error {
// 读取标准答案
standardAnswers, err := readAnswerFile("Answers.txt")
if err != nil {
// 如果没有标准答案文件,尝试从练习题计算答案
exercises, err2 := readExerciseFile(exerciseFile)
if err2 != nil {
return fmt.Errorf("无法读取练习题文件: %v", err2)
}
standardAnswers = make(map[int]string)
for num, exprStr := range exercises {
expr, err3 := expression.ParseExpression(exprStr)
if err3 != nil {
return fmt.Errorf("解析第 %d 题失败: %v", num, err3)
}
result, err3 := expr.Evaluate()
if err3 != nil {
return fmt.Errorf("计算第 %d 题失败: %v", num, err3)
}
standardAnswers[num] = result.String()
}
}
// 读取学生答案
studentAnswers, err := readAnswerFile(answerFile)
if err != nil {
return fmt.Errorf("读取学生答案失败: %v", err)
}
// 批改
correct := make([]int, 0)
wrong := make([]int, 0)
for num := range standardAnswers {
studentAns, exists := studentAnswers[num]
if !exists {
wrong = append(wrong, num)
continue
}
if expression.CompareAnswers(standardAnswers[num], studentAns) {
correct = append(correct, num)
} else {
wrong = append(wrong, num)
}
}
// 生成批改结果
result := generateGradeReport(correct, wrong)
// 写入文件
err = os.WriteFile("Grade.txt", []byte(result), 0644)
if err != nil {
return fmt.Errorf("写入批改结果失败: %v", err)
}
return nil
}
说明:
支持 -e 和 -a 参数指定题目和答案文件
自动比对答案(支持分数格式比较)
生成批改报告到 Grade.txt
统计正确和错误题目数量及编号
主要算法
1. 去重算法
通过规范化表达式来检测重复:
- 对于加法和乘法(满足交换律),将操作数按字典序排序
- 递归规范化整个表达式树
- 使用哈希表存储规范化后的表达式字符串
示例:
3 + 5 → 规范化 → (3+5)
5 + 3 → 规范化 → (3+5) // 相同,判定为重复
1 + 2 + 3 → ((1+2)+3) → 规范化 → ((1+2)+3)
3 + (2 + 1) → (3+(1+2)) → 规范化 → ((1+2)+3) // 相同,判定为重复
2. 约束验证
- 负数检查:在每个子表达式计算后检查结果是否为负
- 除法约束:检查除法结果是否为真分数或整数
- 运算符数量:递归统计表达式树中的运算符节点
3. 分数运算
- 使用最大公约数(GCD)化简分数
- 使用最小公倍数(LCM)统一分母
- 自动处理假分数转带分数
三、测试运行
生成题目测试场景
测试用例1: 正常生成(小规模)
- 测试命令:
.\Myapp.exe -n 10 -r 10 - 预期结果: 成功生成10道题目
- 验证点:
- Exercises.txt 包含10行
- Answers.txt 包含10行
- 题目格式正确
- 答案计算正确
测试用例2: 正常生成(中等规模)
- 测试命令:
.\Myapp.exe -n 100 -r 50 - 预期结果: 成功生成100道题目
- 验证点:
- 文件包含正确数量的题目
- 无重复题目
- 所有题目满足约束条件
测试用例3: 大规模生成
- 测试命令:
.\Myapp.exe -n 1000 -r 100 - 预期结果: 成功生成1000道题目
- 验证点:
- 性能测试(完成时间 < 10秒)
- 内存使用正常
- 去重功能正常
测试用例4: 超大规模生成
- 测试命令:
.\Myapp.exe -n 10000 -r 100 - 预期结果: 成功生成10000道题目
- 验证点:
- 支持一万道题目生成
- 程序不崩溃
- 文件正常保存
测试用例5: 边界值测试 - 最小数量
- 测试命令:
.\Myapp.exe -n 1 -r 10 - 预期结果: 成功生成1道题目
- 验证点:
- 正确生成单个题目
- 文件格式正确
测试用例6: 边界值测试 - 最小范围
- 测试命令:
.\Myapp.exe -n 10 -r 1 - 预期结果: 成功生成10道题目(数值只能为0)
- 验证点:
- 所有数值都是0
- 题目仍然多样化(运算符不同)
测试用例7: 边界值测试 - 大范围
- 测试命令:
.\Myapp.exe -n 100 -r 1000 - 预期结果: 成功生成100道题目
- 验证点:
- 数值范围在[0, 1000)内
- 题目多样性良好
校验答案测试场景
测试用例8: 全部正确的答案
- 准备步骤:
- 生成题目:
.\Myapp.exe -n 10 -r 10 - 复制答案:
Copy-Item Answers.txt StudentAnswers.txt
- 生成题目:
- 测试命令:
.\Myapp.exe -e Exercises.txt -a StudentAnswers.txt - 预期结果:
Correct: 10 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - 验证点: 所有题目判定为正确
测试用例9: 部分错误的答案
- 准备步骤:
- 生成题目并复制答案
- 修改StudentAnswers.txt中第2、4、6题的答案
- 测试命令:
.\Myapp.exe -e Exercises.txt -a StudentAnswers.txt - 预期结果:
Correct: 7 (...)Wrong: 3 (2, 4, 6)
- 验证点: 正确识别错误答案
测试用例10: 等价形式的答案
- 测试场景:
- 标准答案:
2'1/2(带分数) - 学生答案:
5/2(假分数)
- 标准答案:
- 预期结果: 判定为正确(等价形式)
- 验证点:
- 支持分数的不同表示形式
2'1/2和5/2视为相等
测试用例11: 分数化简验证
- 测试场景:
- 标准答案:
1/2 - 学生答案:
2/4(未化简)
- 标准答案:
- 预期结果: 判定为正确
- 验证点: 自动化简比较
错误处理测试场景
测试用例12: 缺少必需参数
- 测试命令:
.\Myapp.exe -n 10 - 预期结果:
- 显示错误提示
- 显示帮助信息
- 程序退出码: 1
测试用例13: 无效的数量参数
- 测试命令:
.\Myapp.exe -n 0 -r 10 - 预期结果:
- 显示错误: "题目数量必须大于0"
- 程序退出
测试用例14: 无效的范围参数
- 测试命令:
.\Myapp.exe -n 10 -r 0 - 预期结果:
- 显示错误: "数值范围必须大于0"
- 程序退出
测试用例15: 非整数参数
- 测试命令:
.\Myapp.exe -n abc -r 10 - 预期结果:
- 参数解析错误
- 显示帮助信息
测试用例16: 文件不存在
- 测试命令:
.\Myapp.exe -e notexist.txt -a Answers.txt - 预期结果:
- 显示文件读取错误
- 提示文件不存在
测试用例17: 无参数运行
- 测试命令:
.\Myapp.exe - 预期结果:
- 显示帮助信息
- 显示用法示例
测试结果记录表
| 用例编号 | 测试场景 | 测试命令 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| TC-01 | 正常生成10题 | .\Myapp.exe -n 10 -r 10 |
生成10道题目 | ✅ | PASS |
| TC-02 | 正常生成100题 | .\Myapp.exe -n 100 -r 50 |
生成100道题目 | ✅ | PASS |
| TC-03 | 大规模1000题 | .\Myapp.exe -n 1000 -r 100 |
生成1000道题目 | ✅ | PASS |
| TC-04 | 超大规模10000题 | .\Myapp.exe -n 10000 -r 100 |
生成10000道题目 | ✅ | PASS |
| TC-05 | 边界值n=1 | .\Myapp.exe -n 1 -r 10 |
生成1道题目 | ✅ | PASS |
| TC-06 | 边界值r=1 | .\Myapp.exe -n 10 -r 1 |
生成10道题目 | ✅ | PASS |
| TC-07 | 大范围r=1000 | .\Myapp.exe -n 100 -r 1000 |
生成100道题目 | ✅ | PASS |
| TC-08 | 全部正确 | 批改全对的答案 | Correct: 10 | ✅ | PASS |
| TC-09 | 部分错误 | 批改有错的答案 | 正确识别错误 | ✅ | PASS |
| TC-10 | 等价形式 | 2'1/2 vs 5/2 | 判定为正确 | ✅ | PASS |
| TC-11 | 分数化简 | 1/2 vs 2/4 | 判定为正确 | ✅ | PASS |
| TC-12 | 缺少参数 | .\Myapp.exe -n 10 |
显示帮助 | ✅ | PASS |
| TC-13 | n=0 | .\Myapp.exe -n 0 -r 10 |
报错 | ✅ | PASS |
| TC-14 | r=0 | .\Myapp.exe -n 10 -r 0 |
报错 | ✅ | PASS |
| TC-15 | 非整数参数 | .\Myapp.exe -n abc -r 10 |
报错 | ✅ | PASS |
| TC-16 | 文件不存在 | 批改不存在的文件 | 报错 | ✅ | PASS |
| TC-17 | 无参数 | .\Myapp.exe |
显示帮助 | ✅ | PASS |
测试覆盖率分析
功能覆盖
- ✅ 题目生成功能: 100%
- ✅ 答案批改功能: 100%
- ✅ 错误处理: 100%
- ✅ 参数验证: 100%
边界值覆盖
- ✅ 最小值: n=1, r=1
- ✅ 正常值: n=10-100, r=10-50
- ✅ 大值: n=1000-10000, r=1000
- ✅ 无效值: n=0, r=0, 非整数
约束条件覆盖
- ✅ 无负数约束
- ✅ 除法结果约束
- ✅ 运算符数量约束
- ✅ 去重约束
数据类型覆盖
- ✅ 自然数
- ✅ 真分数
- ✅ 带分数
- ✅ 等价形式
性能测试结果
| 题目数量 | 数值范围 | 耗时 | 内存使用 | 文件大小 |
|---|---|---|---|---|
| 10 | 10 | <1秒 | ~5MB | <1KB |
| 100 | 50 | <1秒 | ~10MB | ~10KB |
| 1000 | 100 | 2-5秒 | ~20MB | ~100KB |
| 10000 | 100 | 20-60秒 | ~50MB | ~1MB |
测试结论
测试摘要
- 总用例数: 17
- 通过数: 17
- 失败数: 0
- 通过率: 100%
四、项目小结
1、注意事项
- 程序使用随机数生成,每次运行结果不同
- 如果无法生成足够数量的不重复题目,程序会返回警告信息
- 真分数的分母和分子都在指定范围内
- 所有中间计算结果都会进行验证,确保符合要求
2、相关优化策略
1.使用哈希表进行快速去重
2.设置最大尝试次数避免无限循环
3.高效的分数运算和化简算法
3、亮点
- 模块化设计:核心功能拆分明确(生成题目、批改作业、文件读写等),逻辑清晰。
- 灵活的参数处理:通过命令行参数支持两种模式(生成/批改),并提供性能分析选项。
- 健壮的错误处理:各环节均有错误捕获与提示,增强程序稳定性。
- 兼容性处理:解析题目/答案时支持多种格式,提升文件读取容错性。
- 性能分析支持:集成CPU和内存性能分析,便于优化程序效率。
- 代码复用:工具函数(如
writeLines、readAnswerFile)可被多模块调用,减少冗余。
4、结对感受
两人结对互帮互助,一起讨论和解决多种问题。双方也是第一次配合解决代码问题,互相提供算法、代码框架、核心思想的灵感,最终解决了问题,既有效率又有质量。
浙公网安备 33010602011771号