20251029 InputException | 输入异常处理

发布于 10 天前  129 次阅读


关于读入Exception处理

题目:



首先明确,要处理的是以下几种情况:
对于scanf("%d", &x);和要求的范围[0, 100],可能的输入情况是:

  1. 输入一个合法的整数,且在范围内,例如42↲
  2. 输入一个合法的整数,但不在范围内,例如-5↲150↲
  3. 输入一个非整数值,例如abc↲
  4. 输入一个合法的整数,且在范围内,紧跟着有多余的字符,例如50xyz↲
  5. 输入一个合法的整数,但不在范围内,紧跟着有多余的字符,例如150abc↲
  6. 输入一个超出int范围的值,且紧跟着有多余的字符,例如1234567890123abc↲。(cin情况)
  7. 输入一个超出int范围的值,且紧跟着有多余的字符,例如1234567890123abc↲。(scanf情况)

重要信息:任何情况下,约定 cin/scanf 为错误状态才清空当前输入缓冲区中的所有内容,否则不清除。

预期行为:根据demo,它们分别的预期结果是:

  1. 成功读入,输出结果。
  2. 提示错误,要求重新输入。
  3. 提示错误,要求重新输入。
  4. 成功读入,忽略多余字符,输出结果。
  5. 提示错误,要求重新输入。
  6. 提示错误,要求重新输入。
  7. 提示错误,要求重新输入。

下面给出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=1x=42
    正确输入,输出结果。
  • (2.) 形如-5↲150↲等符合输入规范但不符合范围要求的数据:
    ret=1x不在范围内,提示错误,要求重新输入。
  • (4.) 形如50xyz↲等符合输入规范、符合范围要求的数据:
    ret=1x=50,忽略多余字符,输出结果。
    Remark:此时scanf并未失败,缓冲区中的内容仍然存在。因此,抛开本题要求不谈,如果某程序后续还有输入操作,可能会受到影响。因此,清除缓冲区仍然是必要的。

而有一些是需要特别说明的:

  • (3.) 形如abc↲等不符合输入规范的数据,此时scanf失败。
    ret=0x的值保持不变(本例中x保持未初始化,为随机值)。此时题设中任何情况下,约定 cin/scanf 为错误状态才清空当前输入缓冲区中的所有内容,否则不清除。,给出了清除缓冲区的必然性;而scanf失败后,需要进入下一次循环等待新的输入,而缓冲区中内容仍然存在,因此需要清除缓冲区。这给出了清楚缓冲区的必要性。

具体来说,对于缓冲区中的清除是这样实现的:

if (ret == 0) {
    while (getchar() != '\n');
}

scanf失败(ret0)的大前提下:
这里的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进入错误状态。
  • (7.) 输入一个超出int范围的值,且紧跟着有多余的字符,例如1234567890123abc↲。(scanf情况)
    scanf在读取超出int范围的数据时,行为仍然取决于编译器。在gcc下,编译器会将输入的数截断,但值得说明的是,scanf并且不会进入错误状态,即ret1

    • 因此,对于这个例子,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

  1. 本题给了demo程序,但题干表述实在费解,因此上述分析基于不同样例数据在demo下的表现反推而来。一个显著的问题是,对于情况(5.),明显地可以避免第二次循环的发生,使得从输出上表现更合理,但demo并未这样实现。
  2. 对于情况(6.)情况(7.),由于不同编译器的行为不同,以上分析仅基于gcc和常见编译器的行为,其他编译器可能会有不同表现。
  3. 对于本题我们完全可以满足于此,因为在较大数量的测试样例下,与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成功读取123ret=1x=123,但由于123不在范围内,因此第二个条件(x >= 0 && x <= 100)为假,导致整个复合条件为假,因此不会继续判断第三个条件getchar() == '\n',直接进入错误处理逻辑。也就是说,并没有多一次getchar()操作,在第二次循环时,缓冲区中仍然有a↲,导致scanf读取失败,进入清除缓冲区的逻辑。最终在第三次循环时等待用户输入。

样例输入2虽然和预期相符,但也要注意的是,第一次循环时并没有多一次getchar()操作,原因同上。也就是说,第二次循环时,缓冲区中是abc↲,而非bc↲,导致scanf读取失败,进入清除缓冲区的逻辑。最终在第三次循环时等待用户输入。


这里是 /* Huajidawang */ 的个人主页