scanf
很棒,当您知道
[我经常看到人们不鼓励其他人使用scanf
,并说有更好的选择。但是,我最终看到的只是“不要使用scanf
”或“这里是正确的格式字符串”,而且从来没有提到“更好的选择”的任何示例。
例如,让我们看一下这段代码:
scanf("%c", &c);
这将读取最后一次转换后留在输入流中的空白。通常建议的解决方案是使用:
scanf(" %c", &c);
或不使用scanf
。
由于scanf
不好,用于解析scanf
通常可以在不使用scanf
的情况下进行构造的一些ANSI C选项(例如整数,浮点数和字符串)?
最常见的读取输入法是:
使用固定大小的fgets
,通常建议这样做,和
使用fgetc
,在您的示例中将非常有用,因为您只读取一个char
。
要解析输入,可以使用多种功能:
strtok
,以分隔符分隔的字符串
[strtoll
,将字符串转换为整数
[strtof
/d
/ ld
,将字符串转换为浮点数
sscanf
,虽然它确实具有上述所有的缺点,但还不错[[as不错]]] >>
为什么 scanf
在您的问题中不好,我将详细说明:
%n
,%[...]
和%c
,scanf
不会占用空白。这使许多
scanf
的格式使某些人感到困惑。例如,读取unsigned char
整数需要使用%hhd
而不是简单地使用%d
(我注意到,这很合理-否则无法确定类型的大小)。&
的参数时,何时使用一元scanf
运算符会造成混淆。scanf
的返回值非常容易。这很容易导致未定义的行为。scanf
中的缓冲区溢出。 scanf("%s", str)
和可怕的gets
一样糟糕,甚至还不差。scanf
转换整数和浮点数时,您无法检测到溢出。实际上,溢出会导致这些函数发生未定义的行为。scanf
总体上很难正确使用。
scanf
很棒,当您知道
scanf
的最大问题:缓冲区溢出风险
-如果未为%s
和%[
转换说明符指定字段宽度,则可能会导致缓冲区溢出(尝试读取比缓冲区大小更多的输入)保持)。不幸的是,没有一种很好的方法将其指定为参数(与printf
一样)-您必须将其作为转换说明符的一部分进行硬编码,或者进行一些宏设计。 接受应该]被拒绝的输入
-如果您正在使用%d
转换说明符读取输入,并且键入类似12w4
的内容,则您将期望scanf
拒绝该输入,但不会-它成功转换并分配了12
,将w4
留在输入流中以阻止下一次读取。我通常建议使用fgets
以文本形式读取
all交互式输入-它允许您指定一次最多读取的字符数,因此可以轻松防止缓冲区溢出: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
或者您可以处理到目前为止输入的内容,然后重新阅读。这取决于您要解决的问题。
要
tokenize
输入(基于一个或多个定界符将其分割),可以使用strtok
,但要注意-strtok
修改其输入(它使用字符串终止符覆盖定界符),并且您无法保留其状态(即,您不能部分标记一个字符串,然后开始标记另一个字符串,然后从原始字符串中保留的地方开始)。有一个变体strtok_s
,可以保留令牌生成器的状态,但是AFAIK的实现是可选的(您需要检查是否定义了__STDC_LIB_EXT1__
,以查看其是否可用)。 一旦标记了输入,如果需要将字符串转换为数字(即"1234"
=> 1234
),则可以选择。 strtol
和strtod
将整数和实数的字符串表示形式转换为它们各自的类型。它们还使您能够解决上面提到的12w4
问题-它们的一个参数是指向在字符串中转换的第一个字符not
的指针: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;
主要问题是scanf
从未打算处理用户输入。它旨在与完全格式化的数据一起使用。从本质上讲,用户输入是不可预测的。人们可能会合理地问,为什么不应该将用于用户输入的功能从stdin中读取。如果您是经验丰富的* nix用户,则说明不会感到惊讶,但可能会使Windows用户感到困惑。在* nix系统中,构建通过管道运行的程序非常普遍,这意味着您可以通过将第一个程序的stdout管道传输到第二个程序的stdin,将一个程序的输出发送给另一个程序。这样,您可以确保输出以及输入是可预测的。在这种情况下,scanf
实际上可以正常工作。所以为什么没有任何易于使用的标准功能用于用户输入?只能在这里猜测,但我认为那些老的C核心黑客只是认为现有功能足够好,即使它们很笨拙。另外,当您查看典型的终端应用程序时,它们很少会从stdin中读取用户输入。通常,您会将所有用户输入作为命令行参数传递。当然,也有例外,但是对于大多数应用程序来说,用户输入是非常小的事情。
那你该怎么办?
我最喜欢的是
fgets
和sscanf
的组合。我曾经写过一个答案,但是我重新发布了完整的代码。这是一个具有良好错误检查和解析的示例。
#define bsize = 100; char buffer[bsize]; int x,y; float f, g; int r; printf("Enter two integers: "); fflush(stdout); // Make sure the printf is executed before reading if(! fgets(buffer, bsize, stdin)) { fprintf(stderr, "An error occured\n"); exit(1); } if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) { fprintf(stderr, "An error occured. You entered:\n%s\n" buffer); fprintf(stderr, "%d successful conversions", r); exit(1); } printf("Enter a float and a char: "); fflush(stdout); if(! fgets(buffer, bsize, stdin)) { fprintf(stderr, "An error occured\n"); exit(1); } if((r = sscanf(buffer, "%f%f", &f, &g)) != 2) { fprintf(stderr, "An error occured. You entered:\n%s\n" buffer); fprintf(stderr, "%d successful conversions", r ); exit(1); } printf("You entered %d %d %f %f\n", x, y, f, g);
通常,我建议不要期望用户以某种奇怪的格式输入您应该解析为不同变量的输入。如果要分配变量height
和width
,请不要同时输入两个变量。允许用户在它们之间按Enter。
scanf
很棒,当您知道
scanf
从未打算处理用户输入。它旨在与完全格式化的数据一起使用。从本质上讲,用户输入是不可预测的。人们可能会合理地问,为什么不应该将用于用户输入的功能从stdin中读取。如果您是经验丰富的* nix用户,则说明不会感到惊讶,但可能会使Windows用户感到困惑。在* nix系统中,构建通过管道运行的程序非常普遍,这意味着您可以通过将第一个程序的stdout管道传输到第二个程序的stdin,将一个程序的输出发送给另一个程序。这样,您可以确保输出以及输入是可预测的。在这种情况下,scanf
实际上可以正常工作。所以为什么没有任何易于使用的标准功能用于用户输入?只能在这里猜测,但我认为那些老的C核心黑客只是认为现有功能足够好,即使它们很笨拙。另外,当您查看典型的终端应用程序时,它们很少会从stdin中读取用户输入。通常,您会将所有用户输入作为命令行参数传递。当然,也有例外,但是对于大多数应用程序来说,用户输入是非常小的事情。