在do-while循环中验证输入类型

11

基本上,我需要确保输入是一个整数,像这样:

do {
    printf("Enter > ");
    scanf("%d", &integer);
} while (/* user entered a char instead of an int */);

我尝试了各种方法,但当我试图输入一个char时,它总是以运行时错误或无限循环的方式结束。我知道fflush(stdin)是未定义的行为,最好不要将其包含在我的代码中,以防止任何错误,此外由于某些原因,在VS2015中不再起作用

以下是我尝试的方法:

typedef enum {false, true} bool;
int ipt;
char c;
bool wrong_ipt;

do {
    c = '\0';
    printf("Enter > ");
    scanf("%d%c", &ipt, &c); //infinite loop occurs while a char has been entered
} while (c != '\n');

do {
    c = '\0';
    printf("Enter > ");
} while (scanf("%d", &ipt) != EOF);

do {
    wrong_ipt = false;
    do {
        ipt = NULL;
        printf("Enter > ");
        scanf("%d", &ipt);
        if (ipt == NULL) {
            wrong_ipt = true;
            break;
        }
    } while (ipt == NULL);
} while (wrong_ipt);

除了fflush(stdin)之外,还有什么方法可以防止用户在C语言中输入char时导致无限循环吗?

谢谢


为什么不先验证输入? - NullPoiиteя
1
这个有帮助吗?https://dev59.com/JW855IYBdhLWcg3w7pCg。我不想将其列为可能的重复,但这个问题是直接相关的。 - KillaBytes
我尝试编写代码,在输入字符或任何无效输入后立即要求重新输入新的输入。 - Juen Khaw
抱歉,但不行,我正在尝试在C语言中完成它,而不是C++。 - Juen Khaw
5个回答

5

这是一个非常好的例子,说明为什么通常不应该使用scanf来进行用户输入。

由于用户输入是基于行的,因此人们会期望一个输入函数总是一次读取一行输入。然而,scanf函数并不是这样工作的。相反,它只消耗与%d转换格式说明符匹配所需的字符数。如果scanf无法匹配任何内容,则不会消耗任何字符,因此下一次调用scanf将因完全相同的原因失败(假设使用了相同的转换说明符,并且无效输入没有被显式地丢弃)。这就是你代码中出现问题的原因。

在撰写本文时,其他三个答案解决了这个问题,方法是检查scanf的返回值并显式丢弃无效输入。然而,所有这三个答案都有一个问题,即例如接受"6sdfj23jlj"作为数字6的有效输入,尽管在这种情况下整行输入显然应该被拒绝。这是因为scanf,如前面所述,不是每次读取一行输入。

因此,解决你的问题的最佳方法可能是使用基于行的输入,而不是使用fgets。这样,你将始终一次读取一行输入(假设输入缓冲区足够大,可以存储一整行输入)。在读取行后,你可以尝试使用strtol将其转换为数字。即使转换失败,输入行也已从输入流中消耗完毕,因此你将不会遇到上述大部分问题。

使用fgets的简单解决方案可能如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int main( void )
{
    char line[100];
    long number;

    //retry until user enters valid input
    for (;;) //infinite loop, equivalent to while(1)
    {
        char *p;

        //prompt user for input
        printf( "Please enter a number: " );

        //attempt to read one line of input
        if ( fgets( line, sizeof line, stdin ) == NULL )
        {
            fprintf( stderr, "Unrecoverable input error!\n" );
            exit( EXIT_FAILURE );
        }

        //attempt to convert input to number
        number = strtol( line, &p, 10 );

        //verify that conversion was successful
        if ( p == line )
        {
            printf( "Invalid input!\n" );
            continue;
        }

        //verify that remainder of line only contains
        //whitespace, so that input such as "6sdfj23jlj"
        //gets rejected
        for ( ; *p != '\0'; p++ )
        {
            if ( !isspace( (unsigned char)*p ) )
            {
                printf( "Encountered invalid character!\n" );

                //cannot use `continue` here, because that would go to
                //the next iteration of the innermost loop, but we
                //want to go to the next iteration of the outer loop
                goto continue_outer_loop;
            }
        }

        //input was valid, so break out of infinite loop
        break;

    //label for breaking out of nested loop
    continue_outer_loop:
        continue;
    }

    printf( "Input was valid.\n" );
    printf( "The number is: %ld\n", number );

    return 0;
}

请注意,通常不应使用 goto 语句。但是,在此情况下,为了跳出嵌套循环,这是必要的。
此程序输出如下:
Please enter a number: 94hjj
Encountered invalid character!
Please enter a number: 5455g
Encountered invalid character!
Please enter a number: hkh7
Invalid input!
Please enter a number: 6sdfj23jlj
Encountered invalid character!
Please enter a number: 67
Input was valid.
The number is: 67

然而,这段代码仍然不完美。它仍然存在以下问题:
  1. 如果用户在单行中输入了100个字符,则整行内容无法适应输入缓冲区。此时,需要调用两次 fgets 才能读取整行内容,并且程序将错误地将该行视为两个单独的输入行。

  2. 代码没有检查用户输入的数字是否可表示为 long 类型(例如,数字是否过大)。函数 strtol 会通过设置相应的 errno 来报告此问题(这是 scanf 不具备的功能)。

可以通过执行额外的检查和错误处理来解决这两个问题。但是,现在代码变得非常复杂,因此把所有代码放入自己的函数中是有意义的。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <errno.h>

int get_int_from_user( const char *prompt )
{
    //loop forever until user enters a valid number
    for (;;)
    {
        char buffer[1024], *p;
        long l;

        //prompt user for input
        fputs( prompt, stdout );

        //get one line of input from input stream
        if ( fgets( buffer, sizeof buffer, stdin ) == NULL )
        {
            fprintf( stderr, "Unrecoverable input error!\n" );
            exit( EXIT_FAILURE );
        }

        //make sure that entire line was read in (i.e. that
        //the buffer was not too small)
        if ( strchr( buffer, '\n' ) == NULL && !feof( stdin ) )
        {
            int c;

            printf( "Line input was too long!\n" );

            //discard remainder of line
            do
            {
                c = getchar();

                if ( c == EOF )
                {
                    fprintf( stderr, "Unrecoverable error reading from input!\n" );
                    exit( EXIT_FAILURE );
                }

            } while ( c != '\n' );

            continue;
        }

        //attempt to convert string to number
        errno = 0;
        l = strtol( buffer, &p, 10 );
        if ( p == buffer )
        {
            printf( "Error converting string to number!\n" );
            continue;
        }

        //make sure that number is representable as an "int"
        if ( errno == ERANGE || l < INT_MIN || l > INT_MAX )
        {
            printf( "Number out of range error!\n" );
            continue;
        }

        //make sure that remainder of line contains only whitespace,
        //so that input such as "6sdfj23jlj" gets rejected
        for ( ; *p != '\0'; p++ )
        {
            if ( !isspace( (unsigned char)*p ) )
            {
                printf( "Unexpected input encountered!\n" );

                //cannot use `continue` here, because that would go to
                //the next iteration of the innermost loop, but we
                //want to go to the next iteration of the outer loop
                goto continue_outer_loop;
            }
        }

        return l;

    continue_outer_loop:
        continue;
    }
}

int main( void )
{
    int number;

    number = get_int_from_user( "Please enter a number: " );

    printf( "Input was valid.\n" );
    printf( "The number is: %d\n", number );

    return 0;
}

你是对的。验证输入是可取的。然而,如果在输入字符串中使用 strtol 处理超出 long 范围的整数数字,可能会遇到问题。我认为最好的解决方案是逐个字符检查,以便检索到的整数值保持在 LONG_MAX 以下。 - pablo1977
@pablo1977:我认为你对问题的理解过于字面了。虽然OP声明他们想要确保输入是一个“整数”,但在我看来,他们只是想读取一个int,并且想确保输入是有效的。这就是我的答案所完成的任务。但是,如果程序只需要检查输入是否为“整数”,并且如果该整数可能太大而无法表示为long,那么最好的解决方案可能是使用isdigit检查输入的每个字符,并在开头检查+- - Andreas Wenzel
代码相当健壮。但在一些奇特的情况下,例如读取嵌入式_null字符_,如"123\0xyz\n",它可能会被欺骗,但这总是使用fgets()时的挑战。 - chux - Reinstate Monica
@chux:是的,我的代码假设字符编码值为0的字符是空终止字符。所有使用C风格字符串的代码都必须做出这个假设。因此,如果嵌入式空字符确实是一个问题,那么就可以使用不使用C风格字符串的输入函数,例如getcharfread。但是,这将使代码变得更加复杂。 - Andreas Wenzel
@AndreasWenzel getline() 允许使用缓冲区视图和字符串视图,因为缓冲区以 空字符 结尾(可能不是缓冲区中唯一的0),并提供读取长度。fgets() 可以通过预先填充缓冲区的非零值来检测嵌入式0,但这是一个临时解决方案。在C语言中,对输入进行强大的处理是具有挑战性的。鉴于黑客攻击日益增多,我在文本输入中寻找可能包含0的内容时,需要采用一种合理的策略来定义行为并避免未定义行为。 - chux - Reinstate Monica

5
问题在于“scanf()”可能会在您的输入缓冲区中留下未读数据。因此出现“无限循环”的情况。
另一个问题是您应该验证来自 scanf()的返回值。如果您期望一个整数值...并且scanf返回“0”个读取项...则您知道出了些问题。
以下是一个示例:
#include <stdio.h>

void discard_junk () 
{
  int c;
  while((c = getchar()) != '\n' && c != EOF)
    ;
}

int main (int argc, char *argv[])
{
  int integer, i;
  do {
      printf("Enter > ");
      i = scanf("%d", &integer);
      if (i == 1) {
        printf ("Good value: %d\n", integer);
      }
      else {
        printf ("BAD VALUE, i=%i!\n", i);
        discard_junk ();
      }
   } while (i != 1);

  return 0;
}

示例输出:

Enter > A
BAD VALUE, i=0!
Enter > B
BAD VALUE, i=0!
Enter > 1
Good value: 1

希望这有所帮助!

不,这不是一个“bool”。它是“成功匹配和分配的输入项数[由scanf完成],可以少于提供的数量,甚至在早期匹配失败的情况下为零。”:http://linux.die.net/man/3/scanf - paulsm4
但是 discard_junk() 函数如何清理缓冲区呢? - Juen Khaw
1
首先,我想强调 "discard_junk()" 不推荐用于生产代码。我只是想阐明问题。它通过循环调用 getchar() 直到没有东西为止来“工作”。在这里查看 http://cboard.cprogramming.com/c-programming/112873-flushing-buffer.html。另一种选择是使用 fgets()(它总是读取您键入的所有内容)+ sscanf - paulsm4
1
现在有意义了,谢谢。 - Juen Khaw
1
这个答案有一个问题:它接受 6sdfj23jlj 作为数字 6 的有效输入,尽管在这种情况下应该拒绝输入。请参阅我的答案,以获取处理此情况的解决方案。 - Andreas Wenzel
显示剩余2条评论

1
你的基本错误在于从未告诉程序如何消耗无效输入。换句话说,你告诉程序:
  • 若存在整数,则读取它(否则不读取)
  • 如果没有得到一个整数,则回到步骤 1。
我猜你认为你所做的是:
  • 读取输入,如果是整数则存储。
  • 如果不是整数,则回到步骤 1。
因此,你需要重新编写代码,以实现你的意图(或采用其他方法,如另一个答案中所述)。也就是说,你的程序应该:
  • 读取一些输入(例如一行)。
  • 扫描输入是否为整数,并将其存储下来。
  • 如果不是整数,则回到步骤 1。

您可能会发现一些相关函数有用,例如fgetssscanfatoi。(同时,尽量抵制写有错误的代码的诱惑;例如,如果您打算读取一行输入,请确保您实际上做到了,并且做得正确。许多人很懒,在这种情况下会做错事情;例如,只读取一部分行,或者导致缓冲区溢出)


谢谢指出我的错误,因为我并不真正了解scanf()的工作原理。 - Juen Khaw

1
格式说明符 %d 告诉 scanf 在命令行中期望一个整数值。当用户输入一行数据时,它被读取为一个字符串,然后 scanf 尝试理解输入的字符串是否可以解释为整数数字的十进制数。如果这种解释成功,则所找到的值将存储在您传递为参数的整数变量中。 scanf 所做的正确替换次数以一个 int 值的形式被该函数检索。由于您只期望一个输入,因此值 1 表示一切正常。
因此,如果出现错误,例如用户输入了一个非有效的整数数值,则由 scanf 返回的数字小于格式说明符的数量。在这种情况下,小于 1 的值表示发生了错误:
 int ipt, succeded;
 do {
    printf("ipt? ");
    succeded = scanf("%d", &ipt);
    if (succeded < 1) {    // Clean the input
      while (getchar() != '\n') 
        ;
    }
 } while(succeded < 1);

scanf 失败时,您需要清除输入缓冲区以避免无限循环。 - Spikatrix
@CoolGuy:谢谢。我添加了一些清理代码(让我困扰的是,清理代码所占用的行数比实际上有用的代码还要多)。 - pablo1977
这个答案有一个问题:它接受 6sdfj23jlj 作为数字 6 的有效输入,尽管在这种情况下应该拒绝输入。请参阅我的答案,以获取处理此情况的解决方案。 - Andreas Wenzel

-1

我知道回答有点晚了,但你可以减少循环的使用,改用跳转语句,然后只需使用一个if语句来检查错误输入,仍然可以实现相同的结果。

      enter: printf("Enter > ");
      i = scanf("%d", &integer);
      if (i != 1) {
        printf ("BAD VALUE, i=%i!\n", i);
        discard_junk ();
        goto enter:
      }
      printf ("Good value: %d\n", integer);

在正常代码中不鼓励使用goto。我只会在非常特殊的情况下使用goto,比如跳出一些繁琐的嵌套循环(这可能是糟糕编程的症状)。 - pablo1977

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接