关于读入Exception处理
题目:


首先明确,要处理的是以下几种情况:
对于scanf("%d", &x);和要求的范围[0, 100],可能的输入情况是:
- 输入一个合法的整数,且在范围内,例如
42↲。 - 输入一个合法的整数,但不在范围内,例如
-5↲或150↲。 - 输入一个非整数值,例如
abc↲。 - 输入一个合法的整数,且在范围内,紧跟着有多余的字符,例如
50xyz↲。 - 输入一个合法的整数,但不在范围内,紧跟着有多余的字符,例如
150abc↲。 - 输入一个超出
int范围的值,且紧跟着有多余的字符,例如1234567890123abc↲。(cin情况) - 输入一个超出
int范围的值,且紧跟着有多余的字符,例如1234567890123abc↲。(scanf情况)
重要信息:任何情况下,约定 cin/scanf 为错误状态才清空当前输入缓冲区中的所有内容,否则不清除。
预期行为:根据demo,它们分别的预期结果是:
- 成功读入,输出结果。
- 提示错误,要求重新输入。
- 提示错误,要求重新输入。
- 成功读入,忽略多余字符,输出结果。
- 提示错误,要求重新输入。
- 提示错误,要求重新输入。
- 提示错误,要求重新输入。
下面给出C语言样例程序:
C语言实现
#include <stdio.h>
int main() {
int ret, x;
while (1) {
printf("请输入x的值[0-100] : ");
ret = scanf("%d", &x);
if (ret == 1 && (x >= 0 && x <= 100)) {
break;
}
if (ret != 1) {
while (getchar() != '\n');
}
printf ("输入有错[ret=%d x=%d],请重新输入!\n", ret, x);
}
printf("ret=%d x=%d\n", ret, x);
return 0;
}
对于上述的7种情况,有一些的结果是很显然的。
- (1.) 形如
42↲等符合输入规范、符合范围要求的数据:
ret=1,x=42。
正确输入,输出结果。 - (2.) 形如
-5↲或150↲等符合输入规范但不符合范围要求的数据:
ret=1,x不在范围内,提示错误,要求重新输入。 - (4.) 形如
50xyz↲等符合输入规范、符合范围要求的数据:
ret=1,x=50,忽略多余字符,输出结果。
Remark:此时scanf并未失败,缓冲区中的内容仍然存在。因此,抛开本题要求不谈,如果某程序后续还有输入操作,可能会受到影响。因此,清除缓冲区仍然是必要的。
而有一些是需要特别说明的:
- (3.) 形如
abc↲等不符合输入规范的数据,此时scanf失败。
ret=0,x的值保持不变(本例中x保持未初始化,为随机值)。此时题设中任何情况下,约定 cin/scanf 为错误状态才清空当前输入缓冲区中的所有内容,否则不清除。,给出了清除缓冲区的必然性;而scanf失败后,需要进入下一次循环等待新的输入,而缓冲区中内容仍然存在,因此需要清除缓冲区。这给出了清楚缓冲区的必要性。
具体来说,对于缓冲区中的清除是这样实现的:
if (ret == 0) {
while (getchar() != '\n');
}
在scanf失败(ret为0)的大前提下:
这里的while循环的条件是getchar() != '\n',循环体为空。判断循环条件时,都调用getchar()从输入缓冲区中读入一个字符,并判断是否为换行符\n。若不是,则循环条件成立,继续循环;直到读取到换行符\n为止,也就是将本行尚未读入的所有字符都读入并丢弃,从而清空缓冲区。
对于这一情况,最终输出结果是:
请输入x的值[0-100] : abc
输入有错[ret=0 x=633761856],请重新输入!
请输入x的值[0-100] : (光标闪动)
x的值为随机值,因为未初始化。
注意此时成功进入下一次输入,因为在第一次读取失败,ret为0后,清空了缓冲区。
-
(5.) 形如
150abc↲等符合输入规范但不符合范围要求的数据:
这个例子比较微妙,进行了两次完整循环,具体如下:- 第一次循环:
由于150abc以数字开头,则scanf会成功读取150,ret=1,而abc↲仍然留在缓冲区中
要注意的是,此时scanf并未失败,因此按照题目要求,并不会清除缓冲区。循环最后输出错误提示,进入下一次循环。 - 第二次循环:
scanf尝试读取整数,注意!,此时缓冲区中仍然有abc↲!因此此时scanf尝试直接读取缓冲区剩余内容,而非等待用户输入。但由于a不是数字,此时scanf失败,ret=0,进入清除缓冲区的代码,将abc↲清除。循环最后输出错误提示,进入下一次循环。 - 直到第三次循环开头:重新等待用户输入合法数据
对于这个例子,最终输出结果是:请输入x的值[0-100] : 150abc 输入有错[ret=1 x=150],请重新输入! 请输入x的值[0-100] : (scanf再次读入,不等待用户输入,而是直接读取缓存区) 输入有错[ret=0 x=150],请重新输入! 请输入x的值[0-100] : (光标闪动)
- 第一次循环:
-
(6.) 对于
cin形式对数据溢出的处理,这里只给出说明:
cin在不同编译器下的行为可能不同,大多数情况下,cin在读取超出int范围的数据时会进入错误状态(cin == false)。- 多数现代编译器(如g++、msvc等)面对超出上限的值时,对目标变量置最大值
2147483647;面对超出下限的值时,对目标变量置最小值-2147483648。 - 少数编译器(如clang、libc++等)面对超出范围的值时,目标变量保持不变(未定义行为),但
cin进入错误状态。
- 多数现代编译器(如g++、msvc等)面对超出上限的值时,对目标变量置最大值
-
(7.) 输入一个超出
int范围的值,且紧跟着有多余的字符,例如1234567890123abc↲。(scanf情况)
scanf在读取超出int范围的数据时,行为仍然取决于编译器。在gcc下,编译器会将输入的数截断,但值得说明的是,scanf并且不会进入错误状态,即ret为1。- 因此,对于这个例子,
12345678901234abc↲,由于12345678901234超出int范围,scanf对其进行了截断,此时x的值是1942892530,接下来程序的行为也由x的截断值决定。 - 如果
x的截断值在[0, 100]范围内,则程序成功结束,输出截断值。(和情况(3.)相同)
形如:请输入x的值[0-100] : 1125899906842647abcd ret=1 x=23(这是由于
1125899906842647二进制表示是100000000000000000000000000000000000000000000010111。截断后变为23) - 如果
x的随机值不在[0, 100]范围内,则程序输出两次错误提示,经过两次循环后,进入新循环等待输入。(和情况(5.)相同)形如:请输入x的值[0-100] : 12345678901234abc 输入有错[ret=1 x=1942892530],请重新输入! 请输入x的值[0-100] : 输入有错[ret=0 x=1942892530],请重新输入! 请输入x的值[0-100] :
- 因此,对于这个例子,
一些重要Remarks:
- 本题给了
demo程序,但题干表述实在费解,因此上述分析基于不同样例数据在demo下的表现反推而来。一个显著的问题是,对于情况(5.),明显地可以避免第二次循环的发生,使得从输出上表现更合理,但demo并未这样实现。 - 对于
情况(6.)和情况(7.),由于不同编译器的行为不同,以上分析仅基于gcc和常见编译器的行为,其他编译器可能会有不同表现。 - 对于本题,我们完全可以满足于此,因为在较大数量的测试样例下,与
demo的表现相一致。
然而,我们也注意到,对于情况(4.),一方面形如50xyz被当成了正确数处理,也就是如果题目要求形如这样的数应当被视为错误,则本例不能判断这样的Exception。另一方面abc被遗弃在缓冲区中,可能会影响后续的输入操作。
因此我们给出一个改进版本(不能用来交作业!),尽管它和demo的表现在情况(4.)和少数特殊的情况(5.)上有所不同,但这里依然有一些值得我们注意和学习的要点:
#include <stdio.h>
int main() {
int ret, x;
while (1) {
printf("请输入x的值[0-100] : ");
ret = scanf("%d", &x);
if (ret == 1 && (x >= 0 && x <= 100) && getchar() == '\n') {
break;
}
if (ret != 1) {
while (getchar() != '\n');
}
printf ("输入有错[ret=%d x=%d],请重新输入!\n", ret, x);
}
printf("ret=%d x=%d\n", ret, x);
return 0;
}
与前面的示例程序相较,我们只更改了一行if的判断逻辑:
if (ret == 1 && (x >= 0 && x <= 100) && getchar() == '\n')
这里我们增加了一个对getchar()的判断,要求在成功读取整数且范围正确的前提下,下一个读入的字符必须是换行符\n,也就是说,输入缓冲区中不能有多余的字符。如果有多余字符,则getchar()会读入该字符并判断!=\n,导致条件不成立,从而进入错误处理逻辑。
考虑这样写的几种情况:
请输入x的值[0-100] : 12abc #scanf成功读取,ret=1 x=12,此时利用getchar()读取'a',因为'a'!='\n',判断输入错误
输入有错[ret=1 x=12],请重新输入!
请输入x的值[0-100] : 输入有错[ret=0 x=12],请重新输入!#bc↲留在缓存区里,此时scanf读取失败 ret=0 x保持不变为12,开始清空缓存
请输入x的值[0-100] : #进入下一次正常读取
请输入x的值[0-100] : 12a #scanf成功读取,ret=1 x=12,此时利用getchar()读取'a',因为'a'!='\n',判断输入错误
输入有错[ret=1 x=12],请重新输入!
请输入x的值[0-100] : #只有↲留在缓存区里,但由于\n是空白字符,scanf读入输入跳过所有开头空白字符,因此无影响,等待正常读取
你会发现新的程序和之前的程序在逻辑上的一个微小区别:
同样是甩掉“缓冲区”,新的程序由于在成功读取整数后还要检查是否有多余字符,因此会多进行一次getchar()操作,如果数字后的字符只有1位,则可能不会出现第二次循环的情况。
此时我们对下面两个输入的结果进行预测:
样例输入1:
123a↲
样例输入2:
123abc↲
由上述分析,你会自然地觉得,样例输入1只有一个多余字符a,因此由于多了一次getchar(),缓冲区只剩下了↲,因此只会进入一次完整循环,在第二次循环时等待用户输入;
而样例输入2由于由多余字符abc↲,即使多进行一次getchar(),但缓冲区中还是留下了bc↲,因此第二次循环输入失败,总共会进行两次完整循环(多一次清除缓冲区),在第三次循环时等待用户输入。
但实际表现却是:它们都只进行两次完整循环,在第三次循环时等待用户输入
这是为什么?我们还是回到这句话:
if (ret == 1 && (x >= 0 && x <= 100) && getchar() == '\n')
我们注意到这是一个逻辑与&&操作符连接的复合条件判断,而&&操作符具有一个重要特性:由于&&只有在全部条件都为真时才为真,因此在判断时会从左到右依次判断各个条件,一旦遇到某个条件为假,则不再继续判断后续条件,直接返回假。
这样的性质称为短路求值(short-circuit evaluation)。
因此对于样例输入1:
123a↲
在第一次循环时,scanf成功读取123,ret=1,x=123,但由于123不在范围内,因此第二个条件(x >= 0 && x <= 100)为假,导致整个复合条件为假,因此不会继续判断第三个条件getchar() == '\n',直接进入错误处理逻辑。也就是说,并没有多一次getchar()操作,在第二次循环时,缓冲区中仍然有a↲,导致scanf读取失败,进入清除缓冲区的逻辑。最终在第三次循环时等待用户输入。
样例输入2虽然和预期相符,但也要注意的是,第一次循环时并没有多一次getchar()操作,原因同上。也就是说,第二次循环时,缓冲区中是abc↲,而非bc↲,导致scanf读取失败,进入清除缓冲区的逻辑。最终在第三次循环时等待用户输入。



Comments | NOTHING