我能使用什么替代scanf来进行输入转换?

151

我经常看到人们劝阻他人不要使用 scanf 并表示有更好的替代方法。但是,我最终看到的只有 "不要使用 scanf" 或者 "这是正确的格式字符串",从未看到任何提到的“更好的替代方法”的示例。

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

scanf("%c", &c);

这会读取在上一次转换后留在输入流中的空白。通常建议解决此问题的方法是使用:

scanf(" %c", &c);

或者不使用 scanf

既然 scanf 不好用,那么在 ANSI C 中有哪些选项可以将输入格式转换为 scanf 通常可以处理的类型(如整数、浮点数和字符串),而不使用 scanf

9个回答

103

最常见的读取输入的方式有:

  • 使用具有固定大小的fgets,这通常是建议的方法,以及

  • 使用fgetc,如果您只需要读取单个char,则可能会很有用。

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

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

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

  • sscanf比直接使用scanf要好一些,尽管它仍有下面提到的大部分缺陷

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

  • 这可能有些过度,但你可以使用词法分析器和语法分析器(最常见的例子是 flexbison)。

  • 无需转换,只需使用字符串即可。


由于我在问题中没有详细说明为什么scanf不好用,所以我会详细解释一下:

  • 使用转换说明符%[...]%c时,scanf不会吃掉空格。这显然并不广为人知,因为有很多类似的问题

  • 关于何时使用一元运算符&来引用scanf的参数(特别是字符串),存在一些混淆。

  • 很容易忽略从scanf返回的值。这可能会导致从未初始化的变量中读取而造成未定义行为。

  • 很容易忘记在scanf中防止缓冲区溢出。 scanf(“%s”,str)gets一样糟糕,甚至更糟。

  • 无法检测使用scanf转换整数时的溢出情况。 实际上,在这些函数中,溢出会导致未定义的行为



"scanf 不会吃掉空格。" 可以翻译为 "scanf 不会吃掉可选的前导空格。" %[...]%c 都可以轻松读取空格,但也许不是程序员想要的方式。 - chux - Reinstate Monica

68

TL;DR

fgets用于获取输入,sscanf用于解析输入,而scanf试图同时完成这两个任务,这可能会引起问题。先读入再解析。

为什么scanf不好?

scanf的主要问题在于它从未打算处理用户输入。它旨在用于"完美"格式化的数据。我用了"完美"这个词,因为它并不完全正确。但它并不适合解析像用户输入这样不可靠的数据。用户输入是不可预测的。用户可能会误解说明,打错字,意外地在未完成输入之前按下回车等等。有人可能会合理地问为什么一个不应该用于用户输入的函数会从stdin读取。如果您是经验丰富的*nix用户,则解释不会让您感到惊讶,但它可能会让Windows用户感到困惑。在*nix系统中,构建能够通过管道运行的程序非常常见,这意味着您将第一个程序的stdout通过管道传输到第二个程序的stdin以实现输出和输入的可预测性。在这种情况下,scanf的表现良好。但是在处理不可预测的输入时,您可能会遇到各种问题。

为什么没有易于使用的标准用户输入函数呢?只能猜测,我认为老式的 C 黑客们认为现有的函数已经很好了,尽管它们非常笨拙。此外,当你查看典型终端应用程序时,它们非常少从 stdin 读取用户输入。大多数情况下,您将所有的用户输入作为命令行参数传递。当然,也有例外,但对于大多数应用程序来说,用户输入是一个非常小的事情。
那么你可以做些什么呢?首先,gets 不是一个替代方案。它是危险的,永远不应该使用。在这里阅读为什么:为什么 gets 函数如此危险以至于不应该使用? 我最喜欢的是将 fgetssscanf 结合使用。我曾经写过一个关于此的答案,但我将重新发布完整的代码。这是一个具有良好(但不完美)错误检查和解析的示例。它足以用于调试目的。
我不喜欢要求用户在一行上输入两个不同的事物。只有当它们自然地属于彼此时,我才这样做,例如printf("以<美元>.<美分>格式输入价格: "); fgets(buffer, bsize, stdin);,然后使用sscanf(buffer "%d.%d", &dollar, &cent)。我永远不会做像printf("输入三角形的高和底: ")这样的事情。下面使用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);

If you do a lot of these, I could recommend creating a wrapper that always flushes:

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

请注意,(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2 无法检测到尾随的非数字文本。 - chux - Reinstate Monica
1
@chux 修正了 %f%f。第一个是什么意思? - klutt
使用fgets()函数读入"1 2 junk"时,即使输入中有"junk",if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {也不会报告任何错误。 - chux - Reinstate Monica
2
scanf旨在与完美格式化的数据一起使用,但即使如此也不是完全正确的。除了@chux提到的“垃圾”问题之外,还有一个事实,即像“%d%d%d”这样的格式可以从一行,两行或三行(甚至更多,如果有介于空白行)中读取输入,没有办法通过类似“%d \ n%d%d”等方式强制执行(例如)两行输入。 scanf可能适用于格式化的输入,但对于任何基于行的内容都不太好。 - Steve Summit
1
@chqrlie 很好,已更新。 - klutt
显示剩余13条评论

22

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 吞下任何剩余的输入:

How you deal with that is up to you - you can either reject the whole input out of hand, and slurp up any remaining input with getchar:

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

或者你可以处理你已经得到的输入并重新读取。这取决于你尝试解决的问题。

要对输入进行分词(根据一个或多个分隔符拆分它),你可以使用strtok,但要注意- strtok修改其输入(用字符串终止符覆盖分隔符),并且您无法保留其状态(即,您无法部分地标记一个字符串,然后开始标记另一个字符串,然后从原始字符串处恢复)。有一种变体,strtok_s,它可以保留标记器的状态,但是据我所知,它的实现是可选的(您需要检查__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;

如果您没有指定字段宽度或转换抑制(例如%*[%\n],这对于稍后处理过长的行非常有用),则... - Toby Speight
有一种方法可以获取字段宽度的运行时规范,但这并不好。你最终需要在代码中构建格式字符串(可能使用snprintf())。 - Toby Speight
6
你在使用 isspace() 函数时犯了最常见的错误 - 它接受用 int 表示的 无符号 字符,因此你需要将其转换为 unsigned char 以避免在 char 是有符号的平台上产生未定义行为(UB)。 - Toby Speight

11

在这个答案中,我假设你正在阅读和解释文本行。 也许你正在提示用户,他正在输入一些内容并按下RETURN键。或者你正在从某种数据文件中读取结构化文本行。

既然你正在读取文本行,那么围绕一个读取文本行的库函数组织你的代码是有意义的。 标准函数是fgets(),虽然还有其他函数(包括getline)。然后下一步是以某种方式解释该文本行。

以下是调用fgets读取文本行的基本步骤:

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

这段代码简单地读取一行文本并将其打印出来。但是,它有几个限制,我们稍后会讨论到。它还有一个非常好的特性:我们传递给fgets作为第二个参数的数字512是数组line的大小,我们要求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所做的),好吧,在这种情况下,我有点说谎了,重新解释这一行并不那么容易,因此对于问题的这部分答案将需要等待一段时间。

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

(1) 我们一直在称呼

fgets(line, 512, stdin);

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

#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的实际麻烦:

你总是需要指定数组大小。当然,这一点一点也不麻烦 - 这是一个特性,因为缓冲区溢出是非常糟糕的事情。
你必须检查返回值。实际上,这没有什么区别,因为要正确使用scanf,您也必须检查它的返回值。
你必须将\n删除。我承认,这是真正的麻烦。我希望有一个标准函数,我可以指向它而没有这个小问题。(请不要提gets)。但与scanf的17种不同烦恼相比,我每天都会选择fgets的这一个烦恼。
那么你如何删除这个换行符?有许多方法:
(a)显而易见的方法:
char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
(b)巧妙且紧凑的方式:
strtok(line, "\n");

不幸的是,这个在空行上无法正常工作

(c) 另一种紧凑且稍微晦涩的方式:

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

还有其他的方法。我个人总是使用(a),因为它简单明了,虽然不够简洁。 请参见this questionthis question,以获取更多关于从fgets中去除\n的信息。

现在这个问题解决了,我们可以回到另一个我之前跳过的问题: atoi()atof() 的缺陷。这些函数的问题在于它们不会给你任何有用的成功或失败指示:它们会静默地忽略尾随的非数字输入,并且如果根本没有数字输入,它们会静默地返回 0。首选的替代方案 - 还具有某些其他优点 - 是 strtolstrtodstrtol 还允许您使用除 10 以外的基数,这意味着您可以使用 scanf 的效果(包括但不限于)%o%x。但是,正确使用这些函数的方法已经是一个独立的故事,而且会分散注意力,所以我现在不会再多说什么关于它们的内容了。

主要叙述的其余部分涉及您可能正在尝试解析的输入,这些输入比单个数字或字符更复杂。如果您想读取包含两个数字、多个以空格分隔的单词或特定框架标点符号的行,该怎么办?那就很有趣了,如果您尝试使用scanf进行操作,事情可能会变得非常复杂,而且现在您已经使用fgets清晰地读取了一行文本,因此选项远远超过了以前,尽管所有这些选项的完整故事可能填满一本书,但我们只能在这里浅尝辄止。

  1. 我最喜欢的技巧是将文本行分解成由空格分隔的“单词”,然后对每个“单词”进行进一步处理。一个主要的标准函数用于此操作是strtok(它也有其问题,并且需要单独讨论)。我自己更喜欢使用专门的函数来构建指向每个拆分“单词”的指针数组,这个函数我在这些课程笔记中描述了。无论如何,一旦你获得了“单词”,你可以进一步处理每个“单词”,也许使用我们已经看过的相同的atoi/atof/strtol/strtod函数。

  2. 矛盾的是,尽管我们在这里花费了相当多的时间和精力来摆脱scanf,但另一种处理刚刚用fgets读取的文本行的好方法是将其传递给sscanf。通过这种方式,您可以获得大部分scanf的优点,但没有大部分缺点。

  3. 如果您的输入语法特别复杂,则可能适合使用“正则表达式”库进行解析。

  4. 最后,您可以使用任何适合您的特定解析解决方案。您可以使用char *指针逐个字符移动文本行,并检查您期望的字符。或者,您可以使用像strchrstrrchrstrspnstrcspnstrpbrk这样的函数搜索特定字符。或者,您可以使用我们之前跳过的strtolstrtod函数解析/转换并跳过一组数字字符。

当然还有很多可以说的,但是希望这个介绍能够让你入门。


sizeof(line)而不是简单地写sizeof line有什么好的理由吗?前者让它看起来像line是一个类型名! - Toby Speight
1
在编程中,使用sscanf作为转换引擎,但使用不同的工具来收集(并可能修饰)输入,这是一个好的做法。但也许值得在这种情况下提及getline - dmckee --- ex-moderator kitten
当你谈论“fscanf的实际麻烦”时,你是指 fgets 吗? 尤其是有关第三个麻烦让我感到非常恼火,特别是考虑到 scanf 返回一个无用的指向缓冲区的指针,而不是返回输入字符数(这将使去除换行符更加简洁)。 - supercat
1
感谢您解释 sizeof 的用法。对我来说,记住何时需要使用括号很容易:我认为 (type) 就像一个没有值的强制转换(因为我们只关心类型)。还有一件事:您说 strtok(line, "\n") 并不总是有效,但什么情况下无法使用并不明显。我猜您考虑的是行比缓冲区长的情况,所以我们没有换行符,strtok() 返回 null?真遗憾 fgets() 没有返回更有用的值,这样我们就可以知道是否存在换行符了。 - Toby Speight
1
@TobySpeight: 如果一行中只有一个 '\n',则 strtok(line, "\n"); 无法正常工作。这是一个严重的问题!此外,strtok() 对全局隐藏变量具有副作用... 不要使用这种 棘手和虚假 的方法,远离 strtok() - chqrlie
显示剩余7条评论

10

我可以使用什么来解析输入,而不是使用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" -----------------------, &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.
  }

8
让我们将解析的要求陈述如下:
  • 必须接受有效输入(并将其转换为其他形式)

  • 必须拒绝无效输入

  • 当任何输入被拒绝时,有必要提供用户一个描述性信息,解释为什么它被拒绝(用 "普通人" 可以理解的清晰语言) (这样人们就可以找出如何修复问题)

为了保持简单,让我们考虑解析一个单一的简单十进制整数(由用户输入),没有其他内容。用户的输入被拒绝的可能原因包括:

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

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

  • 前导空格和尾随空格将被忽略(例如 "
    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?

编写(潜在地数千行)代码以满足您的要求。

这是唯一的方法。对于绝大多数事情,不要使用libc。 - user426

5
以下是使用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);
}

0

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. 它没有检查输入行是否过长而无法适应缓冲区。

  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

-6
其他答案提供了正确的低级细节,所以我将限制自己在更高层次上进行:首先要分析每个输入行应该是什么样子。尝试用正式语法描述输入 - 幸运的是,您会发现它可以用正则文法或至少是上下文无关文法来描述。如果正则文法足够,那么您可以编写一个有限状态机,它以每个命令行的每个字符识别和解释。然后,您的代码将读取一行(如其他答案中所述),然后通过状态机扫描缓冲区中的字符。在某些状态下,您停止并将扫描到的子字符串转换为数字或其他内容。如果这很简单,您可能可以“自己动手做”,如果您发现需要完整的上下文无关文法,则最好弄清楚如何使用现有的解析工具(例如 lexyacc 或其变体)。

1
有限状态机可能过于复杂;有更简单的方法来检测转换中是否发生溢出(例如在使用 strtoll 后检查 errno == EOVERFLOW)。 - S.S. Anne
1
当Flex可以轻松编写有限状态机时,为什么还要编写自己的有限状态机呢? - jamesqf

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