第三周项目
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694 |
| 这个作业的目标 | 四则运算题目生成与批改程序 |
邱启涵3124005284https://github.com/QQH1115/homework3
四则运算题目生成与批改程序
PSP 表格
| PSP 阶段 | 预估时间(分钟) | 实际时间(分钟) |
|---|---|---|
| 计划 | ||
| · 需求分析 | 20 | 25 |
| · 生成设计文档 | 15 | 20 |
| 开发 | ||
| · 设计复审 | 10 | 15 |
| · 代码规范 | 10 | 10 |
| · 具体设计 | 30 | 40 |
| · 具体编码 | 120 | 180 |
| · 代码复审 | 20 | 30 |
| · 测试 | 40 | 60 |
| 报告 | ||
| · 测试报告 | 15 | 20 |
| · 计算工作量 | 5 | 5 |
| · 事后总结 | 15 | 20 |
| 合计 | 300 | 425 |
一、效能分析
改进思路
第一轮优化:文件 I/O 瓶颈
原代码 filewrite 每写一道题就打开/关闭文件两次,n=10000 时打开/关闭 20000 次。改为文件流提到循环外,只打开一次后,std::basic_filebuf 占比从 57% 降到 0%。
第二轮优化:标准库调用开销
std::to_string换成std::to_chars(不分配内存,直接写缓冲区)std::stoll换成std::from_charsstd::isdigit/std::isspace改成手写字符判断std::string用reserve(64)预留空间- 随机分布对象提到循环外,避免重复构造
性能分析图
优化前:

优化后:

消耗最大的函数: 优化前是 std::basic_filebuf(57%),已消除;当前剩余热点是 main(42%)和 infixToPostfix(15%)。
二、设计实现过程
代码组织
核心模块:
| 模块 | 作用 |
|---|---|
generate(ll r) |
生成一道随机四则运算题目 |
struct Fraction |
分数类,支持 + - * / 和约分 |
calculate(string expr) |
计算表达式,返回分数字符串 |
辅助函数:
| 函数 | 作用 |
|---|---|
wrong() |
打印用法帮助 |
priority(char op) |
返回运算符优先级 |
isOperator(char c) |
判断是否是运算符 |
infixToPostfix(expr) |
中缀转后缀(调度场算法) |
evalPostfix(postfix) |
后缀表达式求值 |
filewrite(file, content) |
追加写文件 |
函数关系
main
├── generate(r) // 生成题目
├── calculate(problem) // 计算答案
│ ├── infixToPostfix // 中缀转后缀
│ │ ├── priority // 优先级判断
│ │ └── isOperator // 运算符判断
│ └── evalPostfix // 后缀求值
│ └── Fraction // 分数运算
└── filewrite // 写入文件
关键函数流程
infixToPostfix(调度场算法):
遍历表达式每个字符:
空格 → 跳过
数字 → 读完整数字(含 a/b 分数),加入输出
左括号 → 入栈
右括号 → 弹出栈顶运算符直到左括号
÷ → 转成 %,按优先级弹出栈顶,% 入栈
普通运算符 → 按优先级弹出栈顶,入栈
遍历结束 → 弹出栈中所有运算符
evalPostfix(后缀求值):
遍历后缀表达式每个 token:
运算符 → 弹出两个操作数,用 Fraction 运算,结果入栈
数字 → 转成 Fraction 入栈
最后栈中只剩一个 Fraction,就是结果
三、代码说明
关键代码 1:generate 生成题目
// 随机决定操作数个数(1~4 个)
ll num = dist14(gen), s = 0;
for (ll i = 0; i < num; i++) {
// 随机决定是否加左括号
if (i != num - 1) {
ll op = dist14(gen);
if (op == 1) { problem += "("; s++; w = 1; }
}
// 生成分数 a/b 并约分
ll a = dist1r(gen), b = dist1r(gen);
ll g = std::gcd(a, b);
a /= g; b /= g;
// 分母为 1 写成整数,否则写成分数
if (b == 1) {
auto [p, ec] = std::to_chars(buf, buf + sizeof(buf), a);
problem.append(buf, p);
} else {
// ... a/b 形式
}
// 随机加右括号
// 随机生成运算符 + - * ÷
}
思路: 用括号栈 s 记录未匹配的左括号数,最后一个操作数后补全所有右括号。
关键代码 2:Fraction 分数类
struct Fraction {
ll num, den;
// 构造函数:保证分母为正,并约分
Fraction(ll n = 0, ll d = 1) : num(n), den(d) {
if (den < 0) { num = -num; den = -den; }
reduce();
}
void reduce() {
ll g = std::gcd(std::abs(num), den);
num /= g; den /= g;
}
Fraction operator+(const Fraction& o) const {
return Fraction(num * o.den + o.num * den, den * o.den);
}
// ... - * /
};
思路: 所有运算都返回新的 Fraction,自动约分。isNegative() 用于检查负数。
关键代码 3:infixToPostfix 中 ÷ 的处理
// 除号 ÷(UTF-8 两字节)→ 转成 %
if ((unsigned char)c == 0xC3 && (unsigned char)expr[i + 1] == 0xB7) {
while (!ops.empty() && priority(ops.top()) >= priority('%')) {
output.push_back(std::string(1, ops.top()));
ops.pop();
}
ops.push('%');
i += 2;
continue;
}
思路: ÷ 和分数里的 / 冲突,所以把 ÷ 转成 % 作为内部符号。priority('/') = 3 比 priority('%') = 2 高,保证 7÷2/9 中的 2/9 被整体读入,不会被拆成 7÷2÷9。
四、测试运行
测试用例(10 个)
| # | 输入 | 预期输出 | 说明 |
|---|---|---|---|
| 1 | 3/4+2/3 |
17/12 |
分数加法 |
| 2 | 1/2*4/5 |
2/5 |
分数乘法 |
| 3 | 5-2/7 |
33/7 |
整数减分数 |
| 4 | (1/3+2)*3/5 |
7/5 |
括号优先级 |
| 5 | 7÷2/9 |
63/2 |
÷ 后跟分数 |
| 6 | 1+2*3 |
7 |
乘法优先级 |
| 7 | (1+2)*3 |
9 |
括号优先级 |
| 8 | 6÷3 |
2 |
整数除法 |
| 9 | 1/2+1/3+1/6 |
1 |
多分数连加 |
| 10 | 2*3/4 |
3/2 |
乘法与分数 |
为什么能确定程序正确
- 分数全程精确:用
ll分子分母,不做浮点运算,1/3+1/6精确得1/2,不会有浮点误差。 - 自动约分:每次运算后
Fraction构造函数自动约分,结果总是最简分数。 - 边界测试:
-n 1 -r 1:最小值测试,不会死循环-n 10000 -r 100:大规模测试,能跑完-n 1000 -r 5:小范围去重测试,触发保护机制
- 负数保护:
evalPostfix每步检查isNegative(),保证计算过程不出负数。 - 括号匹配:
infixToPostfix里检查括号是否匹配,不匹配抛异常。
五、总结
遇到的困难
÷和/冲突:分数用/,除号也用/时无法区分。解决方案:÷内部转成%,/优先级更高。- Windows 编码问题:
÷是 UTF-8 字符,VS 默认 GBK 会乱码,需要另存为 UTF-8 带签名,或加#pragma execution_character_set("utf-8")。 isdigit断言:std::isdigit参数必须是unsigned char,传char时 UTF-8 字符会触发 Debug 断言。解决方案:手写字符判断。- 性能瓶颈:
std::to_string和频繁文件开关是主要瓶颈。解决方案:to_chars、文件流提到循环外。
收获
- 掌握了调度场算法和分数运算的精确实现
- 学会了用 VS 性能分析工具定位瓶颈
- 理解了
to_chars/from_chars比to_string/stoll快的原因 - 认识到文件 I/O 是常见的性能陷阱

浙公网安备 33010602011771号