结对项目
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703 |
| 这个作业的目标 | 完成结对项目要求的程序作业 |
结对项目:小学四则运算题目生成器的设计与实现
肖武阳3124004185,王梓瀚3124004181
GitHub 项目地址:https://github.com/yangyang254/pairwork1
目录
一、题目与需求分析
1.1 需求清单
把题目要求拆成可核对的功能点:
| 编号 | 需求 | 实现 |
|---|---|---|
| 1 | -n 指定题目个数 |
Myapp.exe -n 10 -r 10 |
| 2 | -r 指定数值范围,必须给出 |
缺省时打印错误 + 帮助信息并返回 1 |
| 3 | 计算过程不出现负数 | 减法处强制 e1 >= e2;乘加结果不越界 |
| 4 | e1 ÷ e2 的结果是真分数 |
除法处强制 e1 < e2,即商 < 1 |
| 5 | 每道题运算符不超过 3 个 | 生成时随机 1~3 个运算符,检查程序复核 |
| 6 | 一次运行内题目不重复 | 规范形式 + 哈希表去重 |
| 7 | 题目写 Exercises.txt |
一行一道:题目1: 3 + 4 * 5 = |
| 8 | 答案写 Answers.txt |
一行一道:答案1: 23;真分数 3/5、带分数 2'3/8 |
| 9 | 支持一万道题 | 实测 29 毫秒生成一万道题 |
| 10 | -e/-a 批改并统计 |
结果写 Grade.txt:Correct: n (...) / Wrong: n (...) |
1.2 算术表达式与数据结构
题目给出的文法:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e)
“表达式”天然是递归的,所以内部用二叉树表示一道题:叶子是数(自然数或真分数),
内部结点是一个运算符。这样“生成—求值—打印—解析”四个动作都能在同一份结构上完成。
二、PSP 表格(计划与实际)
时间单位:分钟。计划在动手之前填好,实际在提交之后回填。
2.1 计划(开始实现之前)
| PSP 阶段 | 任务 | 计划时间 |
|---|---|---|
| Planning | 需求分析、确定数据结构与四个版本的拆分 | 40 |
| Design | 表达式树、分数类、生成算法、去重算法设计 | 60 |
| Coding | 第 1 版:参数解析 + 自然数出题 + 文件输出 | 60 |
| Coding | 第 2 版:真分数 + 解析器 + 去重 | 90 |
| Coding | 第 3 版:括号 + 题面自检 + 一万道题 | 60 |
| Coding | 第 4 版:批改统计 + 性能优化 | 60 |
| Test | 编写 test.c(单元测试 + 集成测试) | 60 |
| Test | 编写 check.c(按题目要求逐条检查) | 40 |
| Test | 四个版本各跑一万道题的回归验证 | 30 |
| Analysis | 性能分析、定位耗时最大的函数并优化 | 40 |
| Doc | 写博文、整理提交记录 | 60 |
| 合计 | 600 |
2.2 实际(提交之后)
| PSP 阶段 | 任务 | 实际时间 | 偏差说明 |
|---|---|---|---|
| Planning | 需求分析、版本拆分 | 35 | 需求里有两条表述需要讨论 |
| Design | 数据结构与算法设计 | 70 | 多花了时间想“重复”怎么判 |
| Coding | 第 1 版 | 75 | 为“题面歧义”返工了一次 |
| Coding | 第 2 版 | 120 | 加了表达式解析器,比预估多 |
| Coding | 第 3 版 | 70 | 括号打印规则比想象的细 |
| Coding | 第 4 版 | 55 | 有前面的解析器可以复用 |
| Test | test.c | 85 | 集成用例里踩到工作目录的坑 |
| Test | check.c | 50 | 去重检查用“排序后找相邻相同” |
| Test | 回归验证 | 25 | 自动化脚本立功了 |
| Analysis | 性能分析 | 45 | 自己写了 bench.c 分阶段计时 |
| Doc | 博文与提交记录 | 75 | |
| 合计 | 705 | 比计划多约 17% |
偏差主要出在“第 2 版”和“测试”上:写解析器时低估了分数解析和除零、负数这些边界;
写集成测试时又踩了“子进程当前目录”和“父进程当前目录不是同一个”的坑。
经验是——先把“检查手段”写出来,再回头改代码,返工会少很多。
三、设计实现过程
3.1 代码如何组织
整个项目每个版本都是一个 .c 文件,更易检查,
内部按“自底向上”分成六层,层与层之间只通过函数调用连接,没有全局状态耦合:
- 第 1 版只有:工具层、树、生成、打印、文件输出、参数解析;
- 第 2 版加入:
Frac运算、解析器、规范形式、哈希表; - 第 3 版加入:括号打印、自检、一万道题;
- 第 4 版加入:批改模式、性能优化,并把
main4.c作为最终提交的main.c。
3.2 关键函数与流程图
(1) 生成一道不重复、无歧义的题目
┌──────────────────────────┐
│ gen_expr(随机 1~3 个运算符)│
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ validate_expr:按题意校验 │ 不合法 → 丢弃重来
│ 负数 / 除零 / 商≥1 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ canon_to:算出规范形式 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
┌───▶│ hash_add_unique:是否重复 │ 重复 → 丢弃重来(最多重试 1000 次)
│ └────────────┬─────────────┘
│ ▼
│ ┌──────────────────────────┐
│ │ print_expr:打印题面 │
│ └────────────┬─────────────┘
│ ▼
│ ┌──────────────────────────┐
└────│ parse_line + canon_equal │ 自检失败(题面有歧义)→ 丢弃重来
│ 题面读回来必须还是原树 │
└────────────┬─────────────┘
▼
┌──────────────┐
│ 写入两个文件 │
└──────────────┘
(2) 生成表达式树(递归 + 随机重试)
gen_expr(ops, range):
若 ops == 0:返回一个随机叶子(自然数或真分数),结束
重复最多 RAND_LIMIT 次:
随机选一个运算符 op ∈ {+,-,*,/}
随机把剩下的 ops-1 个运算符分给左右子树
建结点 e = (e_left op e_right)
若 validate_expr(e) 合法:返回 e
否则释放 e,换一个运算符重试
兜底:返回一个叶子,保证一定能返回合法表达式
(3) 批改模式
读一行题目 → 去掉行首编号与行尾 “=”,得到表达式文本
读一行答案 → 去掉行首编号,得到学生答案文本
用“按题意校验”的方式解析题目 → 标准答案;题目本身不合法就记错
用“只求值”的方式解析学生答案 → 学生答案(支持 3/5、2'3/8)
两个分数约分后逐位比较(a 与 a、d 与 d),相等记对
最后写 Grade.txt:Correct: n (编号列表) / Wrong: n (编号列表)
(4) 规范形式:判断“重复”的核心
canon_to(e, buf):
若是叶子:直接写数值(整数写 n,真分数写 a/b,带分数写 n'a/b)
若 e 是 + 或 ×:
把同运算符的操作数全部展开收集(结合律)
对每个操作数递归求规范字符串
排序后拼接成 "( + a b c )" 这样的形式(交换律)
若 e 是 - 或 ÷:
左、右子表达式分别递归,保持顺序拼接 "( - a b )"
这样:
| 题目 | 规范形式 | 是否重复 |
|---|---|---|
23 + 45 / 45 + 23 |
( + 23 45 ) |
重复 |
1+2+3 / 3+(2+1) / 3+2+1 |
( + 1 2 3 ) |
重复 |
3 - 2 / 2 - 3 |
( - 3 2 ) / ( - 2 3 ) |
不重复 |
(1+2)*3 / 1+2*3 |
( * ( + 1 2 ) 3 ) / ( + 1 ( * 2 3 ) ) |
不重复 |
3.3 “除以一个数结果必须是真分数”怎么落地
题目说:如果存在 e1 ÷ e2,那么其结果应是真分数。我们把它严格化为:
e1 ÷ e2 合法 ⟺ e2 ≠ 0,且 e1 < e2(也就是商小于 1,是真分数)
- 生成时:
validate_expr在除法结点上检查frac_is_proper(商),不满足就重抽; - 检查程序
check.c用同样的规则复核(0也算真分数,例如(2-2)/8); - 这样“生成的题”与“检查的规则”完全一致,不会出现“自己生成了自己判不合法”的尴尬。
四、代码说明
下面的代码摘自最终版
main.c,注释为提交版本中的原文。
4.1 分数:用 long long 精确表示,一次运算一次约分
typedef struct
{
long long a; /* 分子 */
long long d; /* 分母,恒大于 0 */
} Frac;
static long long gcd64(long long x, long long y) /* 最大公约数 */
{
long long t;
if (x < 0) x = -x;
if (y < 0) y = -y;
while (y) { t = x % y; x = y; y = t; }
return x;
}
static Frac frac_make(long long a, long long d) /* 构造并约分到最简 */
{
Frac f;
long long g;
if (d < 0) { a = -a; d = -d; } /* 分母恒正,符号放分子 */
g = gcd64(a, d);
if (g == 0) g = 1;
f.a = a / g;
f.d = d / g;
return f;
}
思路:全程用整数运算,避免浮点误差;每次运算后立刻约分,让分子分母不会滚雪球。
测试用例 1/6 + 1/8 = 7/24 就是靠这个保证的。
4.2 求值与按题意校验分开写
/* eval_expr 只负责“算”,不管题目是否合题意 */
static void eval_expr(const Expr *e, Frac *out)
{
Frac a, b;
if (e->op == 0) { *out = e->val; return; }
eval_expr(e->l, &a);
eval_expr(e->r, &b);
switch (e->op)
{
case 1: *out = frac_add(a, b); break;
case 2: *out = frac_sub(a, b); break;
case 3: *out = frac_mul(a, b); break;
default: *out = frac_div(a, b); break;
}
}
/* validate_expr 按题意检查:不出现负数、除数不为 0、除法结果是真分数 */
static int validate_expr(const Expr *e, Frac *out)
{
...
case 2:
if (frac_less(a, b)) return 0; /* e1 >= e2,不出现负数 */
r = frac_sub(a, b);
break;
default:
if (frac_is_zero(b)) return 0; /* 除数不能为 0 */
r = frac_div(a, b);
if (!frac_is_proper(r)) return 0; /* 除法结果必须是真分数 */
break;
...
}
为什么拆成两个函数? 这是第 4 版改批改功能时才想明白的:
批改学生答案时我们只关心“数值对不对”,不想让“结果不是真分数”这种出题规则去否决学生的答案;
而生成题目时又必须严格按规则来。拆开之后,parse_line(text, &v, strict) 用一个
strict 参数就能同时服务两种场景,代码没有再出现重复的解析逻辑。
4.3 括号什么时候必须加
static int need_paren(const Expr *child, const Expr *parent, int is_right)
{
if (child->op == 0)
{
/* 叶子一般不用括号;但如果这个叶子是分数,而父亲是 × 或 ÷,
* 就必须加括号,否则 "80 * 3/4 / 27" 会产生歧义:
* 分不清 3/4 是一个分数,还是 "80 * 3 ÷ 4"。 */
if (child->val.d != 1 && (parent->op == 3 || parent->op == 4)) return 1;
return 0;
}
if (op_prio(child->op) < op_prio(parent->op)) return 1; /* 优先级低要加 */
if (op_prio(child->op) > op_prio(parent->op)) return 0; /* 优先级高不加 */
/* 同优先级时,- 和 / 的右操作数必须加括号:8 / (4 / 2)、7 - (3 - 1) */
return (is_right && (parent->op == 2 || parent->op == 4));
}
思路:括号有三个来源——优先级低、同级但右结合、以及“分数字面量歧义”。
前两条是编译原理里的常规规则,第三条是这个题目特有的坑。
4.4 生成题目时的双自检
put_begin(text, MAX_LINE); /* 自检一:题面能算出同样的答案 */
print_expr(e);
if (parse_line(text, &by_parse, 0) &&
by_parse.a == ans->a && by_parse.d == ans->d &&
canon_equal(e, text)) /* 自检二:题面无歧义 */
return e;
free_expr(e); /* 自检失败,弃用重来 */
这是本项目最有价值的一段代码。 canon_equal 会把题面重新解析成树,
再比较两棵树的规范形式;只要不同,就说明“打印出来的题面”表达的不是“生成的那棵树”。
在加入这段自检之前,我们的一万道题里约有 5% 存在这种歧义;
加入之后,Exercises.txt 与 Answers.txt 永远是一致的。
4.5 去重用的哈希表
static unsigned int hash_str(const char *s)
{
unsigned int h = 2166136261u; /* FNV-1a 哈希 */
while (*s) { h ^= (unsigned char)*s++; h *= 16777619u; }
return h % HASH_SIZE;
}
/* 返回 1 表示新题目(已插入),0 表示重复(重复的不会再插入) */
static int hash_add_unique(const char *key)
{
unsigned int h = hash_str(key);
HashNode *p;
for (p = g_table[h]; p != NULL; p = p->next)
if (strcmp(p->key, key) == 0) return 0; /* 已经出现过 */
p = (HashNode *)malloc(sizeof(HashNode)); /* 新题目,插入链表头 */
p->key = (char *)malloc(strlen(key) + 1);
strcpy(p->key, key);
p->next = g_table[h];
g_table[h] = p;
return 1;
}
思路:判断重复只需要“查得到”,不需要“取得出”,所以用最简单的拉链法哈希表即可,
一次插入平均 O(1),一万道题的判重总耗时只有几毫秒。表的大小取 262144(2 的 18 次方),
用一个静态数组就够,不占栈。
4.6 批改:同一套解析器
if (!parse_line(expr, &std_ans, 1)) /* 题目本身要符合题意 */
{
wrong[n_wrong++] = no;
continue;
}
/* 学生答案支持 3/5、2'3/8 这样的分数写法,只比较数值 */
if (parse_line(user_ans, &my_ans, 0) &&
my_ans.a == std_ans.a && my_ans.d == std_ans.d)
right[n_right++] = no;
else
wrong[n_wrong++] = no;
思路:题目算一遍、学生答案解一遍,两个分数都化为最简后比较分子分母。
这样 6/8 和 3/4 会被判为同一个答案(都是最简 3/4),符合小学批改的直觉。
五、效能分析
5.1 分析手段
题目要求“展示一张性能分析的图”。我们试过用 gprof(gcc -pg),
但在本机的 TDM-GCC 上带 -pg 的程序运行时卡住,因此改为自己写一个分阶段计时程序 bench.c:
它把“生成一道题”拆成 6 个阶段,分别累计耗时,比 gprof 的函数采样更直观。
gcc bench.c -o bench.exe -O2
bench.exe 10000 100
5.2 优化前的数据(一万道题,-r 100)
| 阶段 | 总耗时(ms) | 占比 |
|---|---|---|
| 1. 生成表达式树(含按题意校验) | 5.0 | 15.2% |
| 2. 计算答案 | 0.0 | 0.0% |
| 3. 计算规范形式(去重键) | 6.0 | 18.2% |
| 4. 哈希表查重与存储 | 2.0 | 6.1% |
| 5. 打印题面文本 | 4.0 | 12.1% |
| 6. 解析题面(自检) | 7.0 | 21.2% |
| 合计 | 33 | 约 73% |
5.3 耗时最大的一处:规范形式里的 sprintf
canon_to 对每个叶子都要拼一次字符串,原来是这样写的:
char nb[64];
frac_text(e->val, nb, sizeof(nb)); /* 内部是 snprintf */
strncpy(buf, nb, size - 1);
一万道题里,每道题要算 3~4 次规范形式(去重一次、自检再一次,还有递归子结点),
每次又有 4 个左右的叶子,于是有十几万次 snprintf。改成直接往目标缓冲区写十进制:
static void put_ch(char c)
{
if (g_p < g_end) { *g_p++ = c; *g_p = '\0'; }
}
/* 把整数按十进制直接写进缓冲区,省掉一次 sprintf */
static void put_ll(long long x)
{
char tmp[24];
int n = 0;
if (x < 0) { put_ch('-'); x = -x; }
if (x == 0) { put_ch('0'); return; }
while (x > 0) { tmp[n++] = (char)('0' + (int)(x % 10)); x /= 10; }
while (n > 0) put_ch(tmp[--n]);
}
优化后:
| 阶段 | 优化前(ms) | 优化后(ms) | 变化 |
|---|---|---|---|
| 3. 计算规范形式 | 6.0 | 1~2 | 约 −70% |
| 5. 打印题面文本 | 4.0 | 2~3 | 约 −40% |
| 1. 生成表达式树 | 5.0 | 5.0 | 不变 |
| 6. 解析题面(自检) | 7.0 | 6~7 | 不变(本来就是线性扫描) |
| 整程序 | 33 | 29~31 | 约 −10% |
5.4 其他性能考虑
- 内存:题目按题生成、用完即
free,一万道题不驻留,峰值内存只有哈希表的几 MB; - 文件 I/O:用
fputs逐行写,配合标准库缓冲,一万道题的两个文件约 600 KB,写入时间可忽略; - 重试次数:
-r很小时(例如-r 10)可用的不重复题目本来就少,
程序会重试最多 1000 次后接受重复题,并打印“丢弃的重复题目数”,避免死循环。
六、测试运行
6.1 测试程序与检查程序
项目里有两个测试用的程序(题目允许“生成一个测试项目 C 文件”):
test.c:把main4.c当作库包含进来(用MYAPP_NO_MAIN屏蔽它的main),
直接调用内部函数做单元测试,再用system()启动Myapp.exe做集成测试;check.c:站在“验收者”的角度,按题目要求逐条检查任意一版生成的
Exercises.txt/Answers.txt:格式、数值范围、运算符个数、负数、除法真分数、
答案是否与题目一致、以及题目是否重复(把规范形式排序后找相邻相同)。
gcc test.c -o test.exe -O2 && test.exe
gcc check.c -o check.exe -O2 && check.exe Exercises.txt Answers.txt 10
python verify.py # 一键跑完四个版本的全部检查,输出 verify_log.txt
6.2 十个测试用例
下表是 test.exe 中的代表性用例(共 69 个断言,全部通过):
| 序号 | 用例 | 预期结果 | 说明 |
|---|---|---|---|
| 1 | 1/6 + 1/8 |
7/24 |
题目中给出的真分数运算样例 |
| 2 | 2'3/8 解析 |
19/8 |
带分数(二又八分之三) |
| 3 | 4/8 解析 |
1/2 |
解析后自动约分 |
| 4 | 3 + 4 * 5 |
23 |
先乘后加 |
| 5 | ( 3 + 4 ) * 5 |
35 |
括号优先 |
| 6 | 3 - 5 |
拒绝 | 出现负数,不符合题意 |
| 7 | 5 / 3 |
拒绝 | 除法结果不是真分数 |
| 8 | 3 / 0 |
拒绝 | 除数为 0 |
| 9 | 23+45 与 45+23 |
判为重复 | 交换律 |
| 10 | 1+2+3 与 3+(2+1) |
判为重复 | 结合律 + 交换律 |
| 11 | 3-2 与 2-3 |
不重复 | 减法不满足交换律 |
| 12 | (1+2)*3 与 1+2*3 |
不重复 | 括号改变了意义 |
| 13 | 80 * 3/4 / 27 这类打印 |
必须加括号或丢弃 | 分数字面量歧义的专用用例 |
| 14 | Myapp.exe -n 20 -r 12 |
生成 20 行题目/答案 | 行数、格式、含等号 |
| 15 | 同上,统计运算符个数 | 每道 ≤ 3 个 | 自动跳过分数里的 / |
| 16 | 同上,统计数值 | 全部 < 12 | 数值范围检查 |
| 17 | 把第 3、7、15 题答案改错后批改 | Correct: 17 / Wrong: 3 (3, 7, 15) |
批改统计与编号 |
| 18 | Myapp.exe -n 10000 -r 100 |
正好 10000 行 | 一万道题 |
| 19 | 批改上面的一万道题 | Correct: 10000 |
全对 |
| 20 | 不带 -r 运行 |
返回非 0 且打印帮助 | 参数校验 |
check.exe 的回归结果:
| 版本 | 本次生成 | 题意 | 范围 | 运算符 | 答案 | 重复 |
|---|---|---|---|---|---|---|
v1 -n 200 -r 100(nodup) |
200 道 | 0 | 0 | 0 | 0 | 0 |
v2 -n 200 -r 100(nodup) |
200 道 | 0 | 0 | 0 | 0 | 0 |
v3 -n 10000 -r 10 |
10000 道 | 0 | 0 | 0 | 0 | 0 |
v4 -n 10000 -r 10 |
10000 道 | 0 | 0 | 0 | 0 | 0 |
v4 -n 10000 -r 100 |
10000 道 | 0 | 0 | 0 | 0 | 0 |
表中的数字都是“不符合该检查项的题目数”,全 0 表示全部合格。
另外 python verify.py 的最后会跑 test.exe,结果是 通过 69 个用例,失败 0 个用例。
6.3 运行记录实例
> Myapp.exe -n 10 -r 10
已生成 10 道题目:Exercises.txt / Answers.txt
生成过程中丢弃的重复题目:0 道
总耗时:0.0 毫秒
Exercises.txt Answers.txt
题目1: ( 4/9 + 3 * ( 1/2 ) ) / 4 = 答案1: 35/72
题目2: ( 3/5 ) / 4 = 答案2: 3/20
题目3: 8 * 4 - 1/2 + 3 = 答案3: 34'1/2
题目4: 1/2 - ( 6/7 ) / 2 * ( 2/7 ) = 答案4: 37/98
题目5: ( 4/5 + 2 + 2 ) * ( 1/3 ) = 答案5: 1'3/5
题目6: 5 * 7 + 2 + 1/4 = 答案6: 37'1/4
题目7: 6 - 7 * ( 1/6 ) = 答案7: 4'5/6
题目8: ( 5/8 ) * 1 = 答案8: 5/8
题目9: ( 1/4 ) * ( 3/4 ) - ( 1/3 ) / 7 = 答案9: 47/336
题目10: 6 / 7 / 6 + 1 = 答案10: 1'1/7
> Myapp.exe -e Exercises.txt -a Answers.txt
共批改 10 道题:正确 10 道,错误 0 道,结果已写入 Grade.txt
Correct: 10 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Wrong: 0 ()
手工验算几道:
- 第 1 题:
4/9 + 3 × 1/2 = 4/9 + 3/2 = 35/18,再÷ 4 = 35/72✔ - 第 4 题:
(6/7) ÷ 2 = 3/7,× 2/7 = 6/49,1/2 − 6/49 = 49/98 − 12/98 = 37/98✔ - 第 9 题:
1/4 × 3/4 = 3/16,1/3 ÷ 7 = 1/21,3/16 − 1/21 = 63/336 − 16/336 = 47/336✔
七、项目小结(结对感受)
7.1 做对了什么
- 一开始就把“每个版本该做什么”拆清楚,每个版本都能独立编译、独立运行、独立验收,
出了问题知道是哪一次改动引入的(事实上确实靠这个快速定位了第 1 版的题面歧义); - 先写检查程序,再改功能:
check.c写出来之后,几乎所有需求都能“用程序证明”,
不用靠肉眼一题一题看; - 把“打印”和“解析”当成一对逆运算来设计:最后用“题面自检”把两者锁在一起,
这是整个项目最让我们安心的一段代码; - 性能优化用数据说话:自己写
bench.c分阶段计时,找到了sprintf这个点,
优化后规范形式阶段快了约 70%。
7.2 踩过的坑
| 坑 | 现象 | 教训 |
|---|---|---|
手工拼字符串忘记补 '\0' |
文件里会混入上一个字符串的残留内容,甚至跑出乱码 | 自己管理缓冲区时,“什么时候置 \0”要跟“什么时候写入”一样认真地想 |
没有括号时生成 55 - 38 * 3 |
生成时按从左到右算是 51,读题的人按先乘除算是 −59 | 题面必须能唯一地表达那棵树;做不到就限制生成范围或补括号 |
分数字面量 3/4 与除号 / 混淆 |
80 * 3/4 / 27 有两种读法 |
打印和解析是一对函数,必须一起测 |
| 集成测试里父子进程的工作目录 | system("cd /d dir && app.exe") 改变了子进程目录,父进程读文件却还在原目录 |
子进程和父进程的当前目录是两个东西;测试里统一 _chdir 或全部用绝对路径 |
| 除法结果约束写反 | `if (op == 4 && (a % b != 0 | |
| 检查程序的“重复”判定 | v1/v2 还没做去重,却被判为不合格 | 检查项要跟“这个版本承诺了什么”对齐(我们给 check.exe 加了 nodup) |
7.3 还可以做得更好的地方
- 目前所有代码都在一个文件里,虽然方便提交,但
main.c已经有 900 多行,
如果继续加功能(比如支持负数、支持小数),应该拆成fraction.c、parser.c、generate.c; - 没有做真正的图形界面,
-n、-r、-e、-a都是命令行参数,对小学生不友好; - 没有做题目难度分级,目前
-r是唯一的难度旋钮;
浙公网安备 33010602011771号