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

我经常看到人们劝阻别人不要使用 scanf,并说有更好的选择。然而,我最终看到的不是 “不要使用 scanf就是 “这里有一个正确的格式字符串”,从来没有提到过任何 “更好的选择”的例子。

例如,让我们看看这段代码:

scanf("%c", &c);

这会读取上次转换后留在输入流中的空白。通常建议的解决办法是:

scanf(" %c", &c);

或不使用 scanf

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

24362 次浏览

最常见的阅读输入方式是:

  • 使用通常建议的固定大小的 fgets,以及

  • 使用 fgetc,这可能是有用的,如果你只读一个单一的 char

To convert the input, there are a variety of functions that you can use:

  • strtoll,将字符串转换为整数

  • strtof/d/ld,将字符串转换为浮点数

  • 尽管它确实有下面提到的大部分缺点,但是它并不比使用 scanf作为

  • 在纯 ANSI C 中,没有解析分隔符分隔的输入的好方法。无论是使用 POSIX 中的 strtok_r还是使用线程不安全的 strtok。您也可以使用 strcspnstrspn使用 自己卷吧线程安全变体,因为 strtok_r不涉及任何特殊的操作系统支持。

  • 这可能有点夸张,但是您可以使用 lexer 和解析器(flexbison是最常见的示例)。

  • 没有转换,只是使用字符串


因为我没有准确地进入 为什么 scanf在我的问题中是不好的,我将详细说明:

  • 使用转换说明符 %[...]%cscanf不会占用空格。这显然并不广为人知,正如 这个问题的许多重复所证明的那样。

  • 在引用 scanf的参数(特别是字符串)时,何时使用一元 &操作符有一些混淆。

  • 很容易忽略来自 scanf的返回值。这很容易导致未定义行为读取未初始化的变量。

  • scanf中很容易忘记防止缓冲区溢出。scanf("%s", str)gets一样糟糕,如果不是更糟糕的话。

  • scanf.事实上,溢出会导致这些函数中出现 < a href = “ https://stackoverflow. com/questions/5240789/Scanf-leave-the-new-line-char-In-the-buffer”> 未定义行为 。转换整数时无法检测溢出


当你 知道你的输入总是结构良好,表现良好的时候,scanf是非常棒的。否则..。

在我看来,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;

DR

fgets用于获取输入。sscanf用于事后对其进行解析。scanf试图同时做这两件事。这就是麻烦的处方。先读后解析。

为什么 scanf不好?

主要问题是 scanf从来没有打算处理用户输入。它用于“完美”格式化的数据。我引用“完美”这个词是因为它并不完全正确。但是它的设计目的不是解析像用户输入一样不可靠的数据。本质上,用户输入是不可预测的。用户误解说明,打错字,在完成之前不小心按回车键等等。人们可能会合理地问,为什么不应该用于用户输入的函数要从 stdin读取。如果你是一个经验丰富的 * nix 用户,这个解释不会让你感到惊讶,但是它可能会让 Windows 用户感到困惑。在 * nix 系统中,构建通过管道工作的程序是非常常见的,这意味着您通过将第一个程序的 stdout通过管道传送到第二个程序的 stdin来将一个程序的输出发送给另一个程序。这样,您可以确保输出和输入是可预测的。在这些情况下,scanf实际上工作得很好。但是,当处理不可预测的输入时,就会冒各种各样的风险。

那么,为什么没有任何易于使用的标准函数用于用户输入呢?我们只能在这里猜测,但我认为老的 C 黑客只是认为现有的功能已经足够好了,尽管它们非常笨重。此外,当您查看典型的终端应用程序时,它们很少从 stdin读取用户输入。通常将所有用户输入作为命令行参数传递。当然,也有例外,但对于大多数应用程序来说,用户输入是一件非常小的事情。

那你能做什么?

首先,gets不是一个替代品。它是危险的,永远不应该被使用。阅读原因: 为什么 gets 函数如此危险以至于不应该使用它?

我最喜欢的是 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);

如果你做了很多这样的事情,我可以建议你创建一个总是刷新的包装器:

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,不要同时要求两者。允许用户在它们之间按回车键。此外,这种方法在某种意义上是非常自然的。在输入之前,您永远不会从 stdin获得输入,那么为什么不总是阅读整行呢?当然,如果行比缓冲区长,这仍然会导致问题。我是否记得提到用户输入在 C 语言中是笨拙的?:)

为了避免行长于缓冲区的问题,可以使用自动分配适当大小的缓冲区的函数,可以使用 getline()。缺点是你将需要 free的结果后。标准并不保证这个函数存在,但 POSIX 有。您也可以实现自己的,或者在 SO 上找到一个。如何读取长度未知的输入字符串?

加快进度

如果您真的想用 C 语言创建带有用户输入的程序,我建议您查看一下类似 ncurses的库。因为那样的话,您可能还需要创建带有一些终端图形的应用程序。不幸的是,如果这样做,您将失去一些可移植性,但是它使您能够更好地控制用户输入。例如,它使您能够立即阅读按键,而不是等待用户按回车键。

有趣的阅读

这是一个关于 scanf: https://web.archive.org/web/20201112034702/http://sekrit.de/webdocs/c/beginners-guide-away-from-scanf.html的咆哮

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

  • 必须接受有效的输入(并转换为其他形式)

  • 无效的输入必须被拒绝

  • 当任何输入被拒绝时,有必要向用户提供一个描述性的消息,解释为什么它被拒绝(这样人们就可以找到解决问题的方法)(用明确的“非程序员的普通人很容易理解”的语言)

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

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

我们还要正确定义“输入包含不可接受的字符”,并说:

  • 前面的空格和后面的空格将被忽略(例如:
    「5」将视为「5」)
  • 零或一个小数点(例如: 「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?

自己编写(可能有几千行)代码,以满足您的需求。

在这个答案中,我假设你们正在阅读 解读 一行行文字。 也许您正在提示用户,他正在键入某些内容并且 或者你正在阅读结构化的行 某种数据文件中的文本。

因为你正在阅读一行行的文字,所以组织起来是有意义的 你的代码围绕一个库函数,这个库函数读取一行 短信。 标准函数是 fgets(),尽管还有其他函数(包括 getline) 那一行文字。

下面是调用 fgets读取 文字:

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

这只是读入一行文本并将其打印出来。 如前所述,它有一些局限性,我们将在后面讨论 一分钟。它还有一个很棒的特点: 我们的512号 传递给 fgets的第二个参数是数组的大小 我们要求 fgets解读这个事实,我们可以 告诉 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作为是/否回答) ,你可以直接抓住第一个 行的特征,像这样:

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

(当然,这忽略了用户键入一个 多字符响应; 它悄悄地忽略任何额外的字符 打印出来的。)

最后,如果您希望用户键入一个字符串,则 没有必须包含 空格,如果您想处理输入行

hello world!

作为字符串 "hello"后面跟着其他内容(即 scanf格式的 %s) ,那么,在这种情况下,我 撒了一点小谎,要重新诠释这句台词就没那么容易了 毕竟,这部分问题的答案 等一会儿。

但首先我想回到我跳过的三件事。

我们一直在打电话

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);

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

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

最后,还有一个问题,为了阅读一行文本, fgets读取字符并将其填充到数组中,直到 找到终止行 < em > 的 \n字符,然后填充 数组中的 \n字符也是 你稍微修改了我们前面的例子:

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

如果我运行这个命令并在它提示我的时候输入“ Steve”,它就会打印出来

you typed: "Steve
"

第二行上的 "是因为它读取的字符串和 打印出来的实际上是 "Steve\n"

有时候那个额外的换行符并不重要(比如我们打电话的时候 atoiatof,因为它们都忽略任何额外的非数值 输入后的数字) ,但有时它很重要。所以 通常我们会想要去掉那个新行。有几个 如何做到这一点,我将在一分钟。(我知道我一直 但是我保证,我会回去处理这些事情的。)

此时,您可能会想: “我以为您说的是 scanf 是不好的,另一种方式会更好。 但是 fgets已经开始看起来像个麻烦了。 调用 scanf太容易了! 我不能继续使用它吗?”

当然,如果你愿意,你可以继续使用 scanf 简单的事情,在某些方面更简单。)但是,请不要 当你因为它的17个怪癖之一而失败的时候,来向我哭诉 和弱点,或进入一个无限循环,因为输入您的 没有预料到,或者当你不知道如何使用它做 更复杂的东西。让我们看看 fgets 实际滋扰:

  1. 你总是需要指定数组的大小,当然, 这一点都不麻烦——这是一个特点,因为缓冲 溢出是一件非常糟糕的事情。

  2. 你必须检查返回值。实际上,这是洗, 因为要正确使用 scanf,你必须检查它的返回值 也很有价值。

  3. 你必须把 \n拆下来,我承认,这是真的 真讨厌,真希望我能指出一个标准函数 你对那个没有这个小问题。(请没有人。) 但是与 scanf's相比,scanf's有17个不同之处 讨厌的东西,我随时都可以接受 fgets的这个讨厌的东西。

那么 如何去掉这个新行呢? 有很多方法:

(a)显而易见的方法:

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

(b)精巧紧凑的方法:

strtok(line, "\n");

不幸的是,这个 不太对劲在空行上。

(c)另一种简洁而略显晦涩的方式:

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

还有其他方法。我,我总是使用(a) ,因为它简单明了,如果不够简洁的话。 看到 这个问题,或 这个问题,更多(更多)的剥离 \n从什么 fgets给你。

既然这个问题已经解决了,我们可以继续讨论另一个问题了 我忽略了一件事: atoi()atof()的缺陷。 问题是它们不会给你任何有用的东西 成功或失败的标志: 他们默默地忽视 在非数字输入后面,如果有 没有任何数字输入。首选的替代方案-其中 也有一定的其他优势-是 strtolstrtodstrtol还允许您使用10以外的基数,这意味着您可以 获得 %o%xscanf的效果。 但展示如何正确使用这些函数本身就是一个故事, 而且会分散我们的注意力 变成了一个相当支离破碎的故事,所以我不会说 任何关于他们的消息。

主要叙述的其余部分涉及您可能正在尝试的输入 解析它比单个数字或者 如果你想读一行包含两个 数字,或者多个空格分隔的单词,或者特定的 框架标点符号? 这就是事情变得有趣的地方,而且 如果你努力的话,事情可能会变得很复杂 使用 scanf来做事情 选项,因为您已经使用 fgets清楚地读取了一行文本, 虽然所有这些选择的完整故事可能会填补 一本书,所以我们只能看到表面。

  1. 我最喜欢的技巧是把线分成 空格-分隔“单词”,然后对每个单词做进一步处理 这样做的一个主要标准函数是 strtok(这也有它的问题,也评价了一个整体 我自己的偏好是一个专用函数 用于构造指向每个分离的指针的数组 “单词”,我在里面描述的一个函数 这些课程笔记。 无论如何,一旦你有了“单词”,你可以进一步处理 每一个,可能与相同的 atoi/atof/strtol/strtod 我们已经研究过的功能。

  2. 自相矛盾的是,尽管我们已经花了相当多的钱 花时间和精力想办法摆脱 scanf, 另一种处理我们刚刚阅读的文本行的好方法 fgets就是把它传递给 sscanf scanf的大部分优点,但没有大部分 缺点。

  3. 如果您的输入语法特别复杂,那么可以使用“ regexp”库来解析它。

  4. 最后,您可以使用任何适合的 临时的解析解决方案 您可以使用 检查字符 char *指针。或者可以 使用像 strchrstrrchr这样的函数搜索特定的字符, 或 strspnstrcspnstrpbrk 使用 strtol或 我们之前跳过的 strtod函数。

显然还有很多可以说的,但希望如此 这个介绍会让你开始。

我可以用什么来解析输入而不是 Scanf?

考虑 fgets()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"
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.
}

其他的答案给出了正确的底层细节,所以我将自己限制在一个更高的水平: 首先,分析 你想要什么每个输入行看起来像。试着用一种正式的语法来描述输入——如果幸运的话,你会发现它可以用一个 正常语法来描述,或者至少用一个 上下文无关文法来描述。如果一个规则的语法就足够了,那么您可以编写一个 有限状态机,它可以一次识别和解释每个命令行中的一个字符。然后,您的代码将读取一行(正如其他答复中所解释的那样) ,然后通过状态机扫描缓冲区中的字符。在某些状态下,停止并将目前扫描的子字符串转换为一个数字或其他什么。如果这么简单,你也许可以“自己动手”,如果你发现你需要一个完整的上下文无关文法,你最好弄清楚如何使用现有的解析工具(参考: lexyacc或者它们的变体)。

下面是一个使用 flex扫描简单输入的示例,在本例中是一个 ASCII 浮点数文件,可能是 US (n,nnn.dd)或 European (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);
}

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。如果这个输入没有被丢弃,那么我们将再次遇到所有进一步调用 scanf的问题,%d格式说明符立即失败,这可能会导致无限循环。

即使在输入成功且用户没有输入任何错误输入的情况下,仅仅是 scanf通常在输入流中留下换行符这一事实就可能导致麻烦,如 这个问题所示。

使用 scanf%d格式说明符的另一个问题是,如果转换的结果不能表示为 int(例如,如果结果大于 INT_MAX) ,那么根据 7.21.6.2.10国际标准化组织 C11标准,程序的行为是未定义的,这意味着你不能依赖于任何特定的行为。

为了解决上面提到的所有问题,通常最好使用函数 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. 它不检查输入行是否太长以至于无法放入缓冲区。

  2. 它不检查转换后的数字是否可以表示为 int,例如,该数字是否太大而无法存储在 int中。

  3. 它将接受 6abc作为数字 6的有效输入。这并不像 scanf那样糟糕,因为 scanf将在输入流中保留 abc,而 fgets不会。但是,拒绝输入可能比接受输入更好。

所有这些问题都可以通过采取以下措施来解决:

问题 # 1可以通过检查来解决

  • 输入缓冲区是否包含换行符,或者
  • 是否已到达文件末尾,可以将其视为等效于换行符,因为它还指示行的结束。

问题 # 2可以通过检查函数 strtol是否将 errno设置为宏常量 ERANGE的值来解决,以确定转换后的值是否可以表示为 long。为了确定这个值是否也可以表示为 int,应该将 strtol返回的值与 INT_MININT_MAX进行比较。

问题3可以通过检查行中所有剩余的字符来解决。因为 strtol接受前导 空格字符,所以接受后跟空格字符可能也是合适的。但是,如果输入包含任何其他尾随字符,则可能会拒绝输入。

下面是该代码的改进版本,它解决了上面提到的所有问题,并将所有内容放入一个名为 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