1. 题目背景与需求分析
洛谷P1957口算练习题是一道考察C语言输入处理和基础运算能力的题目。题目要求程序能够处理两种输入格式:
- 运算符+两个操作数(如"a 10 20")
- 仅有两个操作数(如"10 20"),此时运算符沿用上一次的运算符
这道题的难点在于正确处理混合输入格式,特别是当输入行可能包含运算符也可能不包含时,如何准确解析出运算符和操作数。很多初学者会在这里踩坑,因为C语言的scanf函数在不同格式字符串下的行为差异很大。
2. 输入处理方案对比
2.1 常见错误尝试
最初尝试使用scanf("%c %d %d")时,当输入是纯数字(如"10 20")会导致读取失败,因为第一个字符'1'会被当作运算符读取,后面的"0"无法匹配%d格式。
改用scanf("%c%d%d")同样有问题,因为:
- 输入"a 10 20"时,%c读取'a',%d读取10,%d读取20,看似正确
- 但输入"10 20"时,%c读取'1',%d读取0(因为'1'已被读取),%d读取20,完全错误
2.2 字符串解析方案
最终采用的fgets方案是更可靠的选择:
c复制fgets(ch[i], 13, stdin);
fgets会读取整行输入(包括空格和换行符),然后我们可以手动解析这个字符串。这种方法的好处是:
- 不会因为格式不匹配而提前终止读取
- 可以灵活处理不同格式的输入
- 能够准确识别换行符作为输入结束的标志
3. 核心代码解析
3.1 输入处理逻辑
c复制if (ch[i][0] >= 'a' && ch[i][0] <= 'c') {
// 处理带运算符的情况
operator[i] = ch[i][0];
j = 2; // 跳过运算符和后面的空格
// 解析第一个操作数
while (ch[i][j] >= '0' && ch[i][j] <= '9') {
ch[i][j] = ch[i][j] - '0';
arr[i][0] = arr[i][0] * 10 + ch[i][j];
j++;
}
j++; // 跳过空格
// 解析第二个操作数
while (ch[i][j] >= '0' && ch[i][j] <= '9') {
ch[i][j] = ch[i][j] - '0';
arr[i][1] = arr[i][1] * 10 + ch[i][j];
j++;
}
} else {
// 处理不带运算符的情况
operator[i] = operator[i - 1]; // 使用上一个运算符
// 解析两个操作数(逻辑同上)
}
这段代码的关键点:
- 通过检查第一个字符判断是否包含运算符
- 手动解析数字字符串,将字符转换为数字值
- 对于不带运算符的情况,复用上一个运算符
3.2 数字位数计算函数
c复制int Sum(int n) {
int sum = 0;
if (n < 0) {
n = -n;
sum++; // 负号占一个字符
} else if (n == 0) {
sum++; // 0本身占一个字符
}
while (n) {
n = n / 10;
sum++;
}
return sum;
}
这个函数用于计算数字的字符数(包括可能的负号),是输出格式要求的一部分。注意处理了三种情况:
- 正数:直接计算数字位数
- 负数:计算数字位数+1(负号)
- 零:直接返回1
4. 完整实现与测试
4.1 主函数结构
c复制int main() {
int n = 0;
char ch[50][13] = {0};
scanf("%d", &n);
getchar(); // 消耗换行符
char operator[50] = {0};
int arr[50][3] = {0};
// 处理n行输入
for (int i = 0; i < n; i++) {
fgets(ch[i], 13, stdin);
// 输入解析逻辑(见3.1节)
// 根据运算符计算结果
switch (operator[i]) {
case 'a': arr[i][2] = arr[i][0] + arr[i][1]; break;
case 'b': arr[i][2] = arr[i][0] - arr[i][1]; break;
case 'c': arr[i][2] = arr[i][0] * arr[i][1]; break;
}
}
// 输出结果
for (int i = 0; i < n; i++) {
int sum = 2; // 运算符和等号
switch (operator[i]) {
case 'a':
sum += Sum(arr[i][0]) + Sum(arr[i][1]) + Sum(arr[i][2]);
printf("%d+%d=%d\n%d\n", arr[i][0], arr[i][1], arr[i][2], sum);
break;
// 其他运算符处理类似
}
}
return 0;
}
4.2 测试用例
输入示例:
code复制4
a 10 20
10 20
b 5 2
5 2
预期输出:
code复制10+20=30
5
10+20=30
5
5-2=3
3
5-2=3
3
5. 常见问题与调试技巧
5.1 输入缓冲区问题
使用scanf后接fgets时,必须注意scanf会留下换行符在输入缓冲区中。这就是为什么在scanf("%d", &n)后需要调用getchar()来消耗这个换行符,否则第一个fgets会立即读取到一个空行。
5.2 数组越界风险
题目保证n≤50,所以定义char ch[50][13]是安全的。但在实际工程中,应该:
- 检查n的范围
- 考虑使用动态内存分配
- 为fgets提供足够大的缓冲区
5.3 数字解析优化
当前的手动数字解析方法(字符减'0')虽然直观,但效率不高。替代方案:
c复制arr[i][0] = atoi(&ch[i][j]);
while (ch[i][j] >= '0' && ch[i][j] <= '9') j++;
j++; // 跳过空格
5.4 运算符验证
当前代码假设运算符只能是a、b、c。更健壮的实现应该:
- 验证运算符有效性
- 处理无效运算符的情况
- 提供有意义的错误信息
6. 性能优化建议
6.1 减少重复计算
在输出阶段,Sum函数被重复调用多次计算同一个数字的位数。可以预先计算并存储这些值。
6.2 使用更高效的输入方法
对于大规模输入,可以考虑:
- 使用更底层的read函数
- 一次性读取所有输入再处理
- 使用内存映射文件
6.3 输出缓冲
频繁调用printf会影响性能。可以:
- 使用更大的缓冲区
- 先构建完整输出字符串再一次性输出
- 使用puts代替printf输出简单字符串
7. 代码风格改进
7.1 增加注释
虽然算法不复杂,但关键部分应该添加注释说明意图,特别是:
- 输入解析逻辑
- 状态维护(如运算符继承)
- 特殊情况的处理
7.2 提取函数
将输入解析和输出处理逻辑提取为独立函数,提高代码可读性和可维护性。
7.3 错误处理
当前代码假设输入总是合法的。实际应该:
- 检查fgets返回值
- 处理解析失败的情况
- 提供有意义的错误信息
8. 扩展思考
8.1 支持更多运算符
当前只支持加、减、乘。可以轻松扩展支持:
- 除法(注意除零检查)
- 模运算
- 位运算
8.2 浮点数支持
修改数字解析逻辑和Sum函数,可以支持浮点数运算,需要注意:
- 浮点精度问题
- 输出格式控制
- 特殊值(NaN、Inf)处理
8.3 表达式生成
反向问题:给定结果和运算符,生成满足条件的操作数。这可以用于:
- 自动生成练习题
- 测试用例生成
- 数学教学工具开发