我经常看到有人劝阻别人不要使用scanf
,并说有更好的替代品。然而,我最终看到的要么是 “不要使用scanf
“ 要么是 “这是一个正确的格式字符串”,从来没有提到任何 “更好的替代品” 的例子。
例如,让我们看一下这段代码:
scanf("%c", &c);
这个函数读取上次转换后留在输入流中的空格,通常建议的解决方法是用途:
scanf(" %c", &c);
或者不使用scanf
。
既然scanf
是坏的,那么在不使用scanf
的情况下,scanf
通常可以处理的输入格式(例如整数、浮点数和字符串)的转换有哪些ANSI C选项?
9条答案
按热度按时间chhqkbe11#
阅读输入的最常用方法是:
fgets
,这是通常建议的,并且fgetc
,如果只阅读单个char
,这可能会很有用。要转换输入,您可以使用多种函数:
strtoll
,将字符串转换为整数strtof
/d
/ld
,用于将字符串转换为浮点数sscanf
,这并不像简单地使用scanf
那么糟糕,尽管它确实具有下面提到的大多数缺点strtok_r
,要么使用strtok
,后者不是线程安全的。你也可以使用strcspn
和strspn
的roll your own线程安全变体,因为strtok_r
不涉及任何特殊的操作系统支持。flex
和bison
是最常见的例子)。由于我没有在我的问题中详细说明 * 为什么 *
scanf
是坏的,我将详细说明:%[...]
和%c
,scanf
就不会占用空格,这一点显然并不广为人知,正如this question的许多重复项所证明的那样。scanf
的参数(特别是字符串)时,对于何时使用一元&
运算符存在一些困惑。scanf
的返回值是很容易的,这很容易导致阅读未初始化的变量时出现未定义的行为。scanf
中很容易忘记防止缓冲区溢出。scanf("%s", str)
与gets
一样糟糕,如果不是更糟的话。scanf
转换整数时,无法检测溢出。事实上,溢出会导致这些函数中出现undefined behavior。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?我最喜欢的是
fgets
和sscanf
的结合。我曾经写过一个关于这个问题的答案,但是我会重新发布完整的代码。这里是一个不错的(但不完美的)错误检查和解析的例子。它足够好用于调试目的。备注
我并不特别喜欢要求用户在一行中输入两个不同的内容。我只在它们以自然的方式属于对方时才这样做。例如
printf("Enter the price in the format <dollars>.<cent>: "); fgets(buffer, bsize, stdin);
,然后使用sscanf(buffer "%d.%d", &dollar, ¢)
。我永远不会做printf("Enter height and base of the triangle: ")
这样的事情。下面使用fgets
的要点是封装输入,以确保一个输入不会影响下一个输入。如果您经常这样做,我建议您创建一个总是刷新的 Package 器:
这样做可以消除一个常见的问题,那就是尾部换行符可能会扰乱嵌套输入。但是它还有另一个问题,那就是如果行比
bsize
长,你可以用if(buffer[strlen(buffer)-1] != '\n')
检查。如果你想删除换行符,你可以用buffer[strcspn(buffer, "\n")] = 0
。总的来说,我建议不要期望用户以某种奇怪的格式输入,因为您需要将其解析为不同的变量。如果您想分配变量
height
和width
,不要同时要求这两个变量。允许用户在它们之间按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.htmlyyhrrdl83#
当你知道你的输入总是结构良好、行为良好的时候,
scanf
是很棒的。IMO,这里是
scanf
最大的问题:*缓冲区溢出风险-如果不为
%s
和%[
转换说明符指定字段宽度,则有缓冲区溢出风险(试图读取比缓冲器大小所能容纳的更多的输入)。不幸的是,没有什么好方法可以将其指定为参数(与printf
一样)-您必须将其硬编码为转换说明符的一部分,或者执行一些宏恶作剧。*接受 * 应该 * 被拒绝的输入-如果你正在阅读一个带有
%d
转换说明符的输入,并且你键入了类似12w4
的内容,你 * 希望 *scanf
拒绝那个输入,但是它没有--它成功地转换并赋值了12
,把w4
留在输入流中,以扰乱下一次读取。那么,应该用什么来代替呢?
我通常推荐使用
fgets
将 * 所有 * 交互式输入作为文本读取-它允许您指定一次读取的最大字符数,因此您可以轻松地防止缓冲区溢出:fgets
的一个奇怪之处是,如果缓冲区有空间,它会将尾随换行符存储在缓冲区中,因此您可以轻松地检查是否有人键入了比您预期更多的输入:如何处理这个问题取决于你--你可以直接拒绝整个输入,然后用
getchar
处理剩下的输入:或者,你可以处理一下目前为止所输入的内容,然后再读一遍,这取决于你要解决的问题。
将输入 * 标记化 *(根据一个或多个分隔符将其拆分),您可以使用
strtok
,但要注意-strtok
会修改其输入(它用字符串结束符覆盖分隔符),并且您不能保留它的状态(也就是说,你不能部分地标记一个字符串,然后开始标记另一个字符串,然后从你在原始字符串中停止的地方继续)。strtok_s
,它保留了令牌化器的状态,但AFAIK其实现是可选的(您需要检查是否定义了__STDC_LIB_EXT1__
,以查看它是否可用)。在对输入进行标记化之后,如果需要将字符串转换为数字(即,
"1234"
=〉1234
),strtol
和strtod
将整数和真实的的字符串表示转换为它们各自的类型。它们还允许您捕获我上面提到的12w4
问题-它们的参数之一是指向字符串中第一个 * 未 * 转换的字符的指针:hivapdat4#
在这个答案中,我将假设你正在阅读和解释 * 行文本 *。也许你正在提示用户,他正在键入一些东西并点击回车键。或者你正在阅读某种数据文件中的结构化文本行。
既然你读的是一行文本,那么围绕一个能读一行文本的库函数来组织代码是有意义的。标准函数是
fgets()
,尽管还有其他的函数(包括getline
)。然后下一步是以某种方式解释这行文本。下面是调用
fgets
读取一行文本的基本方法:这只需要读入一行文本并打印出来。正如所写的那样,它有一些限制,我们稍后会谈到。它还有一个非常棒的功能:我们传递给
fgets
作为第二个参数的数字512是我们要求fgets
读取的数组line
的大小,这个事实--我们可以告诉fgets
允许读取多少--意味着我们可以确定fgets
不会因为阅读太多而溢出数组。现在我们知道了如何读取一行文本,但是如果我们真的想读取一个整数、一个浮点数、一个字符或一个单词呢?(也就是说,如果我们试图改进的
scanf
调用使用了%d
、%f
、%c
或%s
这样的格式说明符呢?)将一行文本--字符串--重新解释为这些东西是很容易的。要将字符串转换为整数,最简单(尽管不完美)的方法是调用
atoi()
。要转换为浮点数,有atof()
。(还有更好的方法,我们马上就会看到。)下面是一个非常简单的示例:如果你想让用户输入一个字符(可能是
y
或n
作为yes/no响应),你可以直接抓取该行的第一个字符,如下所示:(This当然,忽略用户键入多字符响应的可能性;它会悄悄地忽略键入的任何额外字符。)
最后,如果您希望用户键入一个字符串,绝对不包含空格,如果您希望将输入行
因为字符串
"hello"
后面跟了一些其他的东西(这是scanf``%s
应该做的),那么,在这种情况下,我撒了一点小谎,毕竟用那种方式重新解释这一行并不那么容易,所以这部分问题的答案必须等待一段时间。但首先我想回到我跳过的三件事。
(1)我们一直在打电话
读入数组
line
,其中512是数组line
的大小,所以fgets
知道不会溢出它,但是要确保512是正确的数字(特别是,为了检查是否有人调整了程序来改变大小),你必须读回声明line
的地方,这很麻烦,所以有两种更好的方法来保持大小的同步。你可以,(a)使用预处理器为大小命名:或者,(B)使用C的
sizeof
运算符:(2)第二个问题是我们没有检查错误。当你阅读输入时,你应该 * 总是 * 检查错误的可能性。如果由于某种原因
fgets
不能读取你要求它读取的文本行,它会通过返回一个空指针来指示这一点。所以我们应该这样做最后,还有一个问题,为了读取一行文本,
fgets
读取字符并将其填充到数组中,直到找到终止该行的\n
字符 *,然后将\n
字符也填充到数组中 *。如果我运行这个程序并在提示时键入“Steve”,它将打印出来
第二行的
"
是因为它读取并打印出来的字符串实际上是"Steve\n"
。有时候额外的换行符并不重要(比如我们调用
atoi
或atof
时,因为它们都忽略了数字后面的任何额外的非数字输入),但有时候这很重要,所以我们经常想去掉换行符,有几种方法可以做到这一点,我马上就会讲到。(我知道我已经说过很多次了。但我保证,我会回到所有这些事情上。)此时,您可能会想:“我记得你说过
scanf
不好用,而另一种方式会更好。但是fgets
开始看起来像个麻烦。调用scanf
* 太容易了 *!我不能继续使用它吗?”当然,如果您愿意,可以继续使用
scanf
。(对于 * 真的 * 简单的事情,在某些方面它更简单。)但是,请不要来哭我当它失败了,你由于它的17个怪癖和弱点之一,或进入一个无限循环,因为输入你没有预料到,或者当你不知道如何使用它来做一些更复杂的事情时,让我们看看fgets
的实际麻烦:1.你总是需要指定数组的大小,当然,这一点也不麻烦--这是一个特性,因为缓冲区溢出是一件非常糟糕的事情。
1.你必须检查返回值,实际上,这是白费力气,因为要正确使用
scanf
,你也必须检查它的返回值。1.你必须把
\n
去掉。我承认,这确实是一个麻烦。我希望有一个标准函数,我可以告诉你,没有这个小问题。(请不要提出gets
。)但是与scanf's
的17个不同的麻烦相比,我会接受fgets
的这一个麻烦。那么,你该如何去掉换行符呢?有很多方法:
(a)明显的方式:
(b)巧妙而紧凑的方式:
不幸的是,此doesn't work quite right位于空行上。
(c)另一种简洁而又有点晦涩的方式:
还有其他的方法。我,我总是使用(a),因为它简单明了,如果不够简洁的话。参见this question,或this question,了解更多关于从
fgets
提供给你的东西中剥离\n
的信息。现在这已经不碍事了,我们可以回到我跳过的另一件事上:
atoi()
和atof()
的不完美之处。问题是它们不能给予你任何有用的成功或失败的指示:它们悄悄地忽略尾随的非数字输入,如果根本没有数字输入,它们悄悄地返回0。首选的替代方法--它们也有某些其他优点--是strtol
和strtod
。strtol
还允许使用10以外的基数,也就是说你可以得到(除此之外)%o
或%x
与scanf
。但展示如何正确使用这些函数本身就是一个故事,而且会分散人们的注意力,使他们无法专注于已经变得支离破碎的叙述,所以我现在不打算再多说什么了。剩下的主要叙述关注的是你可能要解析的输入,它比单个数字或字符更复杂。如果你想读一行包含两个数字、多个空格分隔的单词或特定的标点符号,那该怎么办?这就是事情变得有趣的地方,如果你试图使用
scanf
做事情,事情可能会变得复杂。现在您已经使用fgets
清楚地阅读了一行文本,并且有更多的选项,尽管关于所有这些选项的完整故事可能会写满一本书,所以我们在这里只能触及表面。1.我最喜欢的技术是将行分解为空格分隔的“单词”,然后对每个“单词”做进一步的处理。(这也有它的问题,这也值得一个完整的单独讨论)。我自己的首选是一个专用函数,用于构造指向每个分解的“单词”的指针数组,我在these course notes中描述的函数。无论如何,一旦你有了“单词”,你可以进一步处理每一个单词,也许可以使用我们已经看过的相同的
atoi
/atof
/strtol
/strtod
函数。1.矛盾的是,尽管我们在这里花费了大量的时间和精力来解决如何摆脱
scanf
,但另一个处理我们刚刚用fgets
读取的文本行的好方法是将其传递给sscanf
,这样,您就拥有了scanf
的大部分优点,但没有了大部分缺点。1.如果您的输入语法特别复杂,那么使用“regexp”库来解析它可能是合适的。
1.最后,您可以使用任何适合您的 ad hoc 解析解决方案。您可以使用
char *
指针在行中一次移动一个字符,检查您期望的字符。或者您可以使用strchr
或strrchr
、strspn
或strcspn
等函数搜索特定字符。或者您可以使用前面跳过的strtol
或strtod
函数解析/转换并跳过数字字符组。显然还有更多的东西可以说,但希望这篇介绍能让您入门。
omjgkv6w5#
我可以用什么来代替scanf来解析输入?
考虑使用
sscanf(buffer, some_format_and %n, ...)
代替scanf(some_format, ...)
通过使用
" %n"
,代码可以简单地检测是否成功扫描了 * 所有 * 格式,以及是否在末尾没有额外的非空白垃圾。6ovsh4lw6#
让我们将解析的要求表述为:
为了让事情变得简单,我们考虑解析一个简单的十进制整数(由用户输入),而不解析其他任何东西。用户输入被拒绝的可能原因有:
让我们也定义"输入包含不可接受的字符"正确;并说:
5 "将被视为" 5 ")
由此,我们可以确定需要以下错误消息:
从这一点上我们可以看到,一个合适的函数将字符串转换为整数需要区分非常不同类型的错误;并且像"
scanf()
"或"atoi()
"或"strtoll()
"这样的东西是完全和绝对没有价值的,因为它们不能给你任何关于输入的错误的指示(并且使用完全不相关和不适当的关于什么是/不是"有效输入"的定义)。相反,让我们开始写一些有用的东西:
convertStringToInteger()
函数很可能最终变成几百行代码。**现在,这只是"解析一个简单的十进制整数"。想象一下,如果你想解析一些复杂的东西;如"姓名、街道地址、电话号码、电子邮件地址"结构的列表;或者像编程语言一样,在这些情况下,你可能需要写上千行代码来创建一个不像蹩脚笑话的解析。
换句话说...
我可以用什么来代替scanf来解析输入?
自己编写(可能有数千行)代码,以满足您的需求。
rvpgvaaj7#
下面是一个使用
flex
扫描简单输入的例子,在这个例子中,一个ASCII浮点数文件可能是美国(n,nnn.dd
)或欧洲(n.nnn,dd
)格式。这是从一个更大的程序复制的,所以可能有一些未解析的引用:vh0rcniy8#
scanf
最常见的用途之一是读取单个int
作为用户的输入,因此,我的回答将只集中在这一个问题上。下面是一个
scanf
通常如何用于从用户阅读int
的示例:以这种方式使用
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
尝试把字符串转换成整数。下面是一个示例程序:但是,此代码存在以下问题:
1.它不检查输入行是否太长而无法放入缓冲区。
1.它不检查转换后的数字是否可表示为
int
,例如数字是否太大而无法存储在int
中。1.它会接受
6abc
作为数字6
的有效输入,这并不像scanf
那样糟糕,因为scanf
会将abc
留在输入流中,而fgets
不会,但是,拒绝输入可能比接受输入更好。所有这些问题都可以通过执行以下操作来解决:
问题#1可通过检查解决
问题#2可以通过检查函数
strtol
是否将errno
设置为宏常量ERANGE
的值来解决,以确定转换后的值是否可表示为long
。strtol
返回的值应与INT_MIN
和INT_MAX
进行比较。问题#3可以通过检查行中的所有剩余字符来解决。由于
strtol
接受前导whitespace characters,因此接受尾随空格字符可能也是合适的。但是,如果输入包含任何其他尾随字符,则可能应拒绝输入。下面是代码的改进版本,它解决了上面提到的所有问题,并将所有内容放入一个名为
get_int_from_user
的函数中,该函数将自动重新提示用户输入,直到输入有效。此程序具有以下行为:
zdwk9cvp9#
其他答案给予了正确的低层次细节,所以我将自己限制在一个更高层次:首先,分析每一行输入的样子。试着用一个正式的语法来描述输入--幸运的话,你会发现它可以用一个“规则语法”来描述,或者至少是一个“上下文无关语法”。如果一个规则语法就足够了,然后你可以编写一个有限状态机,它一次识别和解释每个命令行中的一个字符。(如其他回复中所解释的),然后通过状态机扫描缓冲区中的字符。在某些状态下,你会停止并将扫描到的子字符串转换为数字或其他。如果这么简单,你可能会“滚动自己的”;如果你发现你需要一个完全上下文无关的语法,你最好弄清楚如何使用现有的解析工具(例如:
lex
和yacc
或它们的变体)。