我可以用什么代替scanf进行输入转换?

mmvthczy  于 2023-01-25  发布在  其他
关注(0)|答案(9)|浏览(251)

我经常看到有人劝阻别人不要使用scanf,并说有更好的替代品。然而,我最终看到的要么是 “不要使用scanf 要么是 “这是一个正确的格式字符串”,从来没有提到任何 “更好的替代品” 的例子。
例如,让我们看一下这段代码:

scanf("%c", &c);

这个函数读取上次转换后留在输入流中的空格,通常建议的解决方法是用途:

scanf(" %c", &c);

或者不使用scanf
既然scanf是坏的,那么在不使用scanf的情况下,scanf通常可以处理的输入格式(例如整数、浮点数和字符串)的转换有哪些ANSI C选项?

chhqkbe1

chhqkbe11#

阅读输入的最常用方法是:

  • 使用具有固定大小的fgets,这是通常建议的,并且
  • 使用fgetc,如果只阅读单个char,这可能会很有用。

要转换输入,您可以使用多种函数:

  • strtoll,将字符串转换为整数
  • strtof/d/ld,用于将字符串转换为浮点数
  • sscanf,这并不像简单地使用scanf那么糟糕,尽管它确实具有下面提到的大多数缺点
  • 在纯ANSI C中没有好的方法来解析分隔符分隔的输入。要么使用POSIX中的strtok_r,要么使用strtok,后者不是线程安全的。你也可以使用strcspnstrspnroll your own线程安全变体,因为strtok_r不涉及任何特殊的操作系统支持。
  • 这样做可能有些过分,但是您可以使用lexer和parser(flexbison是最常见的例子)。
  • 没有转换,只是使用字符串

由于我没有在我的问题中详细说明 * 为什么 * scanf是坏的,我将详细说明:

  • 有了转换说明符%[...]%cscanf就不会占用空格,这一点显然并不广为人知,正如this question的许多重复项所证明的那样。
  • 在引用scanf的参数(特别是字符串)时,对于何时使用一元&运算符存在一些困惑。
  • 忽略scanf的返回值是很容易的,这很容易导致阅读未初始化的变量时出现未定义的行为。
  • scanf中很容易忘记防止缓冲区溢出。scanf("%s", str)gets一样糟糕,如果不是更糟的话。
  • 使用scanf转换整数时,无法检测溢出。事实上,溢出会导致这些函数中出现undefined behavior
i34xakig

i34xakig2#

TL; DR

fgets用于获取输入。sscanf用于以后解析输入。scanf试图同时执行这两项操作。这会带来麻烦。先读取,后解析。

为什么scanf不好?

主要的问题是scanf从来就不是用来处理用户输入的。它是用来处理"完美"格式的数据的。我引用"完美"这个词是因为它并不完全正确。但是它并不是用来解析像用户输入一样不可靠的数据的。从本质上讲,用户输入是不可预测的。用户会误解说明,会打错字,在完成之前不小心按下了回车键等等。有人可能会问为什么一个不应该用于用户输入的函数会从stdin读取。如果你是一个有经验的 * nix用户,这个解释不会让你感到惊讶,但它可能会让Windows用户感到困惑。在 * nix系统中,构建通过管道工作的程序是非常常见的,这意味着通过将第一个程序的stdout管道传输到第二个程序的stdin,将一个程序的输出发送到另一个程序。这样,您可以确保输出和输入是可预测的。在这些情况下,scanf实际上工作得很好,但是当处理不可预测的输入时,你会冒着各种麻烦的风险。
那么,为什么没有任何简单易用的标准函数来支持用户输入呢?这里只能猜测,但我认为老C黑客只是认为现有的函数已经足够好了,尽管它们非常笨拙。此外,当你观察典型的终端应用程序时,它们很少从stdin读取用户输入。大多数情况下,你将所有用户输入作为命令行参数传递。当然,当然也有例外,但是对于大多数应用程序来说,用户输入是非常次要的。

那么你能做些什么?

首先,gets不是一个替代品。它是危险的,不应该使用。阅读这里为什么:Why is the gets function so dangerous that it should not be used?
我最喜欢的是fgetssscanf的结合。我曾经写过一个关于这个问题的答案,但是我会重新发布完整的代码。这里是一个不错的(但不完美的)错误检查和解析的例子。它足够好用于调试目的。

备注

我并不特别喜欢要求用户在一行中输入两个不同的内容。我只在它们以自然的方式属于对方时才这样做。例如printf("Enter the price in the format <dollars>.<cent>: "); fgets(buffer, bsize, stdin);,然后使用sscanf(buffer "%d.%d", &dollar, &cent)。我永远不会做printf("Enter height and base of the triangle: ")这样的事情。下面使用fgets的要点是封装输入,以确保一个输入不会影响下一个输入。

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%f%f", &f, &g)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

如果您经常这样做,我建议您创建一个总是刷新的 Package 器:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}

这样做可以消除一个常见的问题,那就是尾部换行符可能会扰乱嵌套输入。但是它还有另一个问题,那就是如果行比bsize长,你可以用if(buffer[strlen(buffer)-1] != '\n')检查。如果你想删除换行符,你可以用buffer[strcspn(buffer, "\n")] = 0
总的来说,我建议不要期望用户以某种奇怪的格式输入,因为您需要将其解析为不同的变量。如果您想分配变量heightwidth,不要同时要求这两个变量。允许用户在它们之间按Enter键。另外,这种方法在某种意义上是很自然的,你永远不会从stdin得到输入,直到你按下回车键,那么为什么不总是读取整行呢?当然,如果行比缓冲区长,这仍然会导致问题。我记得提到过用户输入在C中很笨拙吗?:)
为了避免行比缓冲区长的问题,你可以使用一个函数自动分配一个适当大小的缓冲区,你可以使用getline()。缺点是你将需要free的结果之后。这个函数不保证存在的标准,但POSIX有它。你也可以实现自己的,或找到一个SO.How can I read an input string of unknown length?
∮加快游戏节奏∮
如果你真的想用C语言创建用户输入的程序,我建议你看看ncurses这样的库。因为你可能还想创建带有一些终端图形的应用程序。不幸的是,如果你这样做,你会失去一些可移植性,但它能让你更好地控制用户输入。例如,它使您能够立即读取按键,而不是等待用户按Enter键。
□有趣的阅读
下面是一个关于scanf的咆哮:https://web.archive.org/web/20201112034702/http://sekrit.de/webdocs/c/beginners-guide-away-from-scanf.html

yyhrrdl8

yyhrrdl83#

当你知道你的输入总是结构良好、行为良好的时候,scanf是很棒的。
IMO,这里是scanf最大的问题:

*缓冲区溢出风险-如果不为%s%[转换说明符指定字段宽度,则有缓冲区溢出风险(试图读取比缓冲器大小所能容纳的更多的输入)。不幸的是,没有什么好方法可以将其指定为参数(与printf一样)-您必须将其硬编码为转换说明符的一部分,或者执行一些宏恶作剧。
*接受 * 应该 * 被拒绝的输入-如果你正在阅读一个带有%d转换说明符的输入,并且你键入了类似12w4的内容,你 * 希望 * scanf拒绝那个输入,但是它没有--它成功地转换并赋值了12,把w4留在输入流中,以扰乱下一次读取。

那么,应该用什么来代替呢?
我通常推荐使用fgets将 * 所有 * 交互式输入作为文本读取-它允许您指定一次读取的最大字符数,因此您可以轻松地防止缓冲区溢出:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

fgets的一个奇怪之处是,如果缓冲区有空间,它会将尾随换行符存储在缓冲区中,因此您可以轻松地检查是否有人键入了比您预期更多的输入:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

如何处理这个问题取决于你--你可以直接拒绝整个输入,然后用getchar处理剩下的输入:

while ( getchar() != '\n' ) 
  ; // empty loop

或者,你可以处理一下目前为止所输入的内容,然后再读一遍,这取决于你要解决的问题。
将输入 * 标记化 *(根据一个或多个分隔符将其拆分),您可以使用strtok,但要注意-strtok会修改其输入(它用字符串结束符覆盖分隔符),并且您不能保留它的状态(也就是说,你不能部分地标记一个字符串,然后开始标记另一个字符串,然后从你在原始字符串中停止的地方继续)。strtok_s,它保留了令牌化器的状态,但AFAIK其实现是可选的(您需要检查是否定义了__STDC_LIB_EXT1__,以查看它是否可用)。
在对输入进行标记化之后,如果需要将字符串转换为数字(即,"1234" =〉1234),strtolstrtod将整数和真实的的字符串表示转换为它们各自的类型。它们还允许您捕获我上面提到的12w4问题-它们的参数之一是指向字符串中第一个 * 未 * 转换的字符的指针:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;
hivapdat

hivapdat4#

在这个答案中,我将假设你正在阅读和解释 * 行文本 *。也许你正在提示用户,他正在键入一些东西并点击回车键。或者你正在阅读某种数据文件中的结构化文本行。
既然你读的是一行文本,那么围绕一个能读一行文本的库函数来组织代码是有意义的。标准函数是fgets(),尽管还有其他的函数(包括getline)。然后下一步是以某种方式解释这行文本。
下面是调用fgets读取一行文本的基本方法:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

这只需要读入一行文本并打印出来。正如所写的那样,它有一些限制,我们稍后会谈到。它还有一个非常棒的功能:我们传递给fgets作为第二个参数的数字512是我们要求fgets读取的数组line的大小,这个事实--我们可以告诉fgets允许读取多少--意味着我们可以确定fgets不会因为阅读太多而溢出数组。
现在我们知道了如何读取一行文本,但是如果我们真的想读取一个整数、一个浮点数、一个字符或一个单词呢?(也就是说,如果我们试图改进的scanf调用使用了%d%f%c%s这样的格式说明符呢?)
将一行文本--字符串--重新解释为这些东西是很容易的。要将字符串转换为整数,最简单(尽管不完美)的方法是调用atoi()。要转换为浮点数,有atof()。(还有更好的方法,我们马上就会看到。)下面是一个非常简单的示例:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

如果你想让用户输入一个字符(可能是yn作为yes/no响应),你可以直接抓取该行的第一个字符,如下所示:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(This当然,忽略用户键入多字符响应的可能性;它会悄悄地忽略键入的任何额外字符。)
最后,如果您希望用户键入一个字符串,绝对不包含空格,如果您希望将输入行

hello world!

因为字符串"hello"后面跟了一些其他的东西(这是scanf``%s应该做的),那么,在这种情况下,我撒了一点小谎,毕竟用那种方式重新解释这一行并不那么容易,所以这部分问题的答案必须等待一段时间。
但首先我想回到我跳过的三件事。
(1)我们一直在打电话

fgets(line, 512, stdin);

读入数组line,其中512是数组line的大小,所以fgets知道不会溢出它,但是要确保512是正确的数字(特别是,为了检查是否有人调整了程序来改变大小),你必须读回声明line的地方,这很麻烦,所以有两种更好的方法来保持大小的同步。你可以,(a)使用预处理器为大小命名:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

或者,(B)使用C的sizeof运算符:

fgets(line, sizeof(line), stdin);

(2)第二个问题是我们没有检查错误。当你阅读输入时,你应该 * 总是 * 检查错误的可能性。如果由于某种原因fgets不能读取你要求它读取的文本行,它会通过返回一个空指针来指示这一点。所以我们应该这样做

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

最后,还有一个问题,为了读取一行文本,fgets读取字符并将其填充到数组中,直到找到终止该行的\n字符 *,然后将\n字符也填充到数组中 *。

printf("you typed: \"%s\"\n", line);

如果我运行这个程序并在提示时键入“Steve”,它将打印出来

you typed: "Steve
"

第二行的"是因为它读取并打印出来的字符串实际上是"Steve\n"
有时候额外的换行符并不重要(比如我们调用atoiatof时,因为它们都忽略了数字后面的任何额外的非数字输入),但有时候这很重要,所以我们经常想去掉换行符,有几种方法可以做到这一点,我马上就会讲到。(我知道我已经说过很多次了。但我保证,我会回到所有这些事情上。)
此时,您可能会想:“我记得你说过scanf不好用,而另一种方式会更好。但是fgets开始看起来像个麻烦。调用scanf * 太容易了 *!我不能继续使用它吗?”
当然,如果您愿意,可以继续使用scanf。(对于 * 真的 * 简单的事情,在某些方面它更简单。)但是,请不要来哭我当它失败了,你由于它的17个怪癖和弱点之一,或进入一个无限循环,因为输入你没有预料到,或者当你不知道如何使用它来做一些更复杂的事情时,让我们看看fgets的实际麻烦:

1.你总是需要指定数组的大小,当然,这一点也不麻烦--这是一个特性,因为缓冲区溢出是一件非常糟糕的事情。
1.你必须检查返回值,实际上,这是白费力气,因为要正确使用scanf,你也必须检查它的返回值。
1.你必须把\n去掉。我承认,这确实是一个麻烦。我希望有一个标准函数,我可以告诉你,没有这个小问题。(请不要提出gets。)但是与scanf's的17个不同的麻烦相比,我会接受fgets的这一个麻烦。
那么,你该如何去掉换行符呢?有很多方法:
(a)明显的方式:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(b)巧妙而紧凑的方式:

strtok(line, "\n");

不幸的是,此doesn't work quite right位于空行上。
(c)另一种简洁而又有点晦涩的方式:

line[strcspn(line, "\n")] = '\0';

还有其他的方法。我,我总是使用(a),因为它简单明了,如果不够简洁的话。参见this question,或this question,了解更多关于从fgets提供给你的东西中剥离\n的信息。
现在这已经不碍事了,我们可以回到我跳过的另一件事上:atoi()atof()的不完美之处。问题是它们不能给予你任何有用的成功或失败的指示:它们悄悄地忽略尾随的非数字输入,如果根本没有数字输入,它们悄悄地返回0。首选的替代方法--它们也有某些其他优点--是strtolstrtodstrtol还允许使用10以外的基数,也就是说你可以得到(除此之外)%o%xscanf。但展示如何正确使用这些函数本身就是一个故事,而且会分散人们的注意力,使他们无法专注于已经变得支离破碎的叙述,所以我现在不打算再多说什么了。
剩下的主要叙述关注的是你可能要解析的输入,它比单个数字或字符更复杂。如果你想读一行包含两个数字、多个空格分隔的单词或特定的标点符号,那该怎么办?这就是事情变得有趣的地方,如果你试图使用scanf做事情,事情可能会变得复杂。现在您已经使用fgets清楚地阅读了一行文本,并且有更多的选项,尽管关于所有这些选项的完整故事可能会写满一本书,所以我们在这里只能触及表面。
1.我最喜欢的技术是将行分解为空格分隔的“单词”,然后对每个“单词”做进一步的处理。(这也有它的问题,这也值得一个完整的单独讨论)。我自己的首选是一个专用函数,用于构造指向每个分解的“单词”的指针数组,我在these course notes中描述的函数。无论如何,一旦你有了“单词”,你可以进一步处理每一个单词,也许可以使用我们已经看过的相同的atoi/atof/strtol/strtod函数。
1.矛盾的是,尽管我们在这里花费了大量的时间和精力来解决如何摆脱scanf,但另一个处理我们刚刚用fgets读取的文本行的好方法是将其传递给sscanf,这样,您就拥有了scanf的大部分优点,但没有了大部分缺点。
1.如果您的输入语法特别复杂,那么使用“regexp”库来解析它可能是合适的。
1.最后,您可以使用任何适合您的 ad hoc 解析解决方案。您可以使用char *指针在行中一次移动一个字符,检查您期望的字符。或者您可以使用strchrstrrchrstrspnstrcspn等函数搜索特定字符。或者您可以使用前面跳过的strtolstrtod函数解析/转换并跳过数字字符组。
显然还有更多的东西可以说,但希望这篇介绍能让您入门。

omjgkv6w

omjgkv6w5#

我可以用什么来代替scanf来解析输入?
考虑使用sscanf(buffer, some_format_and %n, ...)代替scanf(some_format, ...)
通过使用" %n",代码可以简单地检测是否成功扫描了 * 所有 * 格式,以及是否在末尾没有额外的非空白垃圾。

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ----------------> " %n" -----------------------, &n
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }
6ovsh4lw

6ovsh4lw6#

让我们将解析的要求表述为:

  • 必须接受有效输入(并转换为其他形式)
  • 必须拒绝无效输入
      • 当任何输入被拒绝时,有必要向用户提供一个描述性消息,解释(以清楚的"非程序员的普通人容易理解的"语言)为什么它被拒绝(以便人们能够找出如何解决问题)**

为了让事情变得简单,我们考虑解析一个简单的十进制整数(由用户输入),而不解析其他任何东西。用户输入被拒绝的可能原因有:

  • 输入包含不可接受的字符
  • 输入表示低于可接受的最小值的数字
  • 输入表示大于可接受的最大值的数字
  • 输入表示具有非零小数部分的数

让我们也定义"输入包含不可接受的字符"正确;并说:

  • 前导空格和尾随空格将被忽略(例如"

5 "将被视为" 5 ")

  • 允许0或1个小数点(例如,"1234."和"1234.000"均视为"1234")
  • 必须至少有一个数字(例如.""被拒绝)
  • 不允许超过一个小数点(例如,拒绝"1.2.3")
  • 不在数字之间的逗号将被拒绝(例如,",1234"将被拒绝)
  • 小数点后的逗号将被拒绝(例如,"1234.000,000"将被拒绝)
  • 另一个逗号后面的逗号将被拒绝(例如,"1,234"将被拒绝)
  • 所有其他逗号将被忽略(例如,"1,234"将被视为"1234")
  • 将拒绝不是第一个非空白字符的减号
  • 拒绝不是第一个非空白字符的正号

由此,我们可以确定需要以下错误消息:

  • "输入开始处有未知字符"
  • "输入末尾有未知字符"
  • "输入中间有未知字符"
  • "数字太小(最小值为....)"
  • "数字太大(最大值为....)"
  • "数字不是整数"
  • "小数点太多"
  • "无小数位"
  • "数字开头的逗号错误"
  • "数字末尾的逗号错误"
  • "数字中间的逗号错误"
  • "小数点后逗号错误"

从这一点上我们可以看到,一个合适的函数将字符串转换为整数需要区分非常不同类型的错误;并且像"scanf()"或"atoi()"或"strtoll()"这样的东西是完全和绝对没有价值的,因为它们不能给你任何关于输入的错误的指示(并且使用完全不相关和不适当的关于什么是/不是"有效输入"的定义)。
相反,让我们开始写一些有用的东西:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}
    • 满足规定的要求;这个convertStringToInteger()函数很可能最终变成几百行代码。**

现在,这只是"解析一个简单的十进制整数"。想象一下,如果你想解析一些复杂的东西;如"姓名、街道地址、电话号码、电子邮件地址"结构的列表;或者像编程语言一样,在这些情况下,你可能需要写上千行代码来创建一个不像蹩脚笑话的解析。
换句话说...
我可以用什么来代替scanf来解析输入?
自己编写(可能有数千行)代码,以满足您的需求。

rvpgvaaj

rvpgvaaj7#

下面是一个使用flex扫描简单输入的例子,在这个例子中,一个ASCII浮点数文件可能是美国(n,nnn.dd)或欧洲(n.nnn,dd)格式。这是从一个更大的程序复制的,所以可能有一些未解析的引用:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.

%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}
vh0rcniy

vh0rcniy8#

scanf最常见的用途之一是读取单个int作为用户的输入,因此,我的回答将只集中在这一个问题上。
下面是一个scanf通常如何用于从用户阅读int的示例:

int num;

printf( "Please enter an integer: " );

if ( scanf( "%d", &num ) != 1 )
{
    printf( "Error converting input!\n" );
}
else
{
    printf( "The input was successfully converted to %d.\n", num );
}

以这种方式使用scanf存在几个问题:
函数scanf并不总是读取整行输入。
如果由于用户输入了错误的输入(如abc)而导致输入转换失败,则错误的输入将留在输入流中。如果之后不丢弃此错误的输入,则所有使用%d说明符对scanf的进一步调用将立即失败,而不等待用户输入进一步的输入。这可能会导致无限循环。
即使输入转换成功,任何尾随的错误输入也将留在输入流中。例如,如果用户输入6abc,则scanf将成功转换6,但将abc留在输入流中。如果不丢弃此输入,那么我们将再次遇到所有进一步调用具有%d格式说明符的scanf立即失败的问题,这可能导致无限循环。
即使在输入成功并且用户没有输入任何尾随错误输入的情况下,scanf通常会在输入流中留下换行符这一事实也会引起麻烦,如this question中所示。
scanf%d说明符一起使用的另一个问题是,如果转换结果不能表示为int(例如,如果结果大于INT_MAX),则根据ISO C11标准§7.21.6.2 ¶10,程序的行为是未定义的,这意味着您不能依赖任何特定的行为。
为了解决上面提到的所有问题,通常最好使用函数fgets,如果可能的话,它总是一次读取整行输入。这个函数将把输入作为字符串读取。完成这个操作后,你可以使用函数strtol尝试把字符串转换成整数。下面是一个示例程序:

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

int main( void )
{
    char line[200], *p;
    int num;

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

    //attempt to read one line of input
    if ( fgets( line, sizeof line, stdin ) == NULL )
    {
        printf( "Input failure!\n" );
        exit( EXIT_FAILURE );
    }

    //attempt to convert string to integer
    num = strtol( line, &p, 10 );
    if ( p == line )
    {
        printf( "Unable to convert to integer!\n" );
        exit( EXIT_FAILURE );
    }

    //print result
    printf( "Conversion successful! The number is %d.\n", num );
}

但是,此代码存在以下问题:
1.它不检查输入行是否太长而无法放入缓冲区。
1.它不检查转换后的数字是否可表示为int,例如数字是否太大而无法存储在int中。
1.它会接受6abc作为数字6的有效输入,这并不像scanf那样糟糕,因为scanf会将abc留在输入流中,而fgets不会,但是,拒绝输入可能比接受输入更好。
所有这些问题都可以通过执行以下操作来解决:
问题#1可通过检查解决

  • 输入缓冲区是否包含换行符,或
  • 是否已经到达文件结尾,这可以被视为等同于换行符,因为它也指示行的结尾。

问题#2可以通过检查函数strtol是否将errno设置为宏常量ERANGE的值来解决,以确定转换后的值是否可表示为longstrtol返回的值应与INT_MININT_MAX进行比较。
问题#3可以通过检查行中的所有剩余字符来解决。由于strtol接受前导whitespace characters,因此接受尾随空格字符可能也是合适的。但是,如果输入包含任何其他尾随字符,则可能应拒绝输入。
下面是代码的改进版本,它解决了上面提到的所有问题,并将所有内容放入一个名为get_int_from_user的函数中,该函数将自动重新提示用户输入,直到输入有效。

#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 "6abc" 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( "Enter a number: " );

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

    return 0;
}

此程序具有以下行为:

Enter a number: abc
Error converting string to number!
Enter a number: 6000000000
Number out of range error!
Enter a number: 6 7 8
Unexpected input encountered!
Enter a number: 6abc
Unexpected input encountered!
Enter a number: 6
Input was valid.
The number is: 6
zdwk9cvp

zdwk9cvp9#

其他答案给予了正确的低层次细节,所以我将自己限制在一个更高层次:首先,分析每一行输入的样子。试着用一个正式的语法来描述输入--幸运的话,你会发现它可以用一个“规则语法”来描述,或者至少是一个“上下文无关语法”。如果一个规则语法就足够了,然后你可以编写一个有限状态机,它一次识别和解释每个命令行中的一个字符。(如其他回复中所解释的),然后通过状态机扫描缓冲区中的字符。在某些状态下,你会停止并将扫描到的子字符串转换为数字或其他。如果这么简单,你可能会“滚动自己的”;如果你发现你需要一个完全上下文无关的语法,你最好弄清楚如何使用现有的解析工具(例如:lexyacc或它们的变体)。

相关问题