你至少有三个误解:
- “char 变量存储一个空格”
-
rewind(stdin) 清除缓冲区
-
_Placeholder 告诉你一些有趣的事情,关于 scanf 如何处理空格
但是,很抱歉,这些都不是真的。
让我们回顾一下scanf实际上是如何处理空格的。我们从两个重要的背景信息开始:
- 换行符
\n 在大多数方面是一个普通的空白字符。它像任何其他字符一样占用输入缓冲区中的空间。当您按下 Enter 键时,它会到达输入缓冲区。
- 解析完
% 指令后,scanf 总是在输入流中留下未解析的输入。
假设你写
int a, b;
scanf("%d%d", &a, &b);
假设您运行该代码并键入作为输入
12 34
然后按 Enter 键。会发生什么?
首先,输入流 (stdin) 现在包含六个字符:
"12 34\n"
scanf 首先处理你给它的两个%d 指令中的第一个。它扫描字符1 和2,将它们转换为整数12 并将其存储在变量a 中。它在它看到的第一个非数字字符处停止读取,这是2 和3 之间的空格字符。输入流现在是
" 34\n"
注意空格字符仍在输入流中。
scanf 接下来处理第二个%d 指令。它不会立即找到数字字符,因为空格字符仍然存在。但这没关系,因为像大多数(但不是全部)scanf 格式指令一样,%d 有一个秘密的额外功能:它会在读取和转换整数之前自动跳过空白字符。所以第二个%d读取并丢弃空格字符,然后读取字符3和4并将它们转换为整数34,并将其存储在变量b中。
现在scanf 完成了。输入流只包含换行符:
"\n"
接下来,让我们看一个稍微不同的——尽管我们将会看到,实际上非常相似——的例子。假设你写
int x, y;
scanf("%d", &x);
scanf("%d", &y);
假设您运行该代码并键入作为输入
56
78
(在两行中,这意味着您按 Enter 两次)。
现在会发生什么?
在这种情况下,输入流最终会包含这六个字符:
"56\n78\n"
第一个scanf 调用有一个%d 指令要处理。它扫描字符5 和6,将它们转换为整数56 并将其存储在变量x 中。它在它看到的第一个非数字字符处停止读取,这是6 之后的换行符。输入流现在是
"\n78\n"
请注意,换行符(两个换行符)仍在输入流中。
现在第二个scanf 调用运行。它也有一个要处理的%d 指令。输入流上的第一个字符不是数字:它是换行符。但这没关系,因为%d 知道如何跳过空格。所以它读取并丢弃换行符,然后读取字符7和8并将它们转换为整数78,并将其存储在变量y中。
现在第二个scanf 完成了。输入流只包含换行符:
"\n"
这可能都是有道理的,可能看起来并不令人惊讶,可能会让你觉得,“好吧,有什么大不了的?”重要的是:在这两个示例中,输入都包含最后一个换行符。
假设,稍后在您的程序中,您有一些其他输入要读取。我们现在来到了一个非常重要的决策点:
-
如果下一个输入调用是对scanf 的另一个调用,并且如果它涉及(许多)格式说明符之一,该格式说明符具有跳过空格的秘密额外功能,则该格式说明符将跳过换行符,然后执行扫描和转换换行符之后的任何输入的工作,程序将按您的预期运行。
-
但是如果下一个输入调用不是对scanf的调用,或者如果它是对scanf的调用,它涉及为数不多的没有秘密额外权力的输入说明符之一,换行将不被“跳过”;它将被读取为实际输入。如果下一个输入调用是getchar,它将读取并返回换行符。如果下一个输入调用是fgets,它将读取并返回一个空行。如果下一个输入调用是带有%c 指令的scanf,它将读取并返回换行符。如果下一个输入调用是带有%[^\n] 指令的scanf,它将读取一个空行。 (实际上,%[^\n] 在这种情况下会读取 nothing,因为它会将 \n 留在输入中。)
在最后一种情况下,“额外”空格会导致问题。在最后一种情况下,您发现自己需要显式“刷新”或丢弃多余的空格。
在没有陷入所有血腥细节的情况下,事实证明,刷新或丢弃scanf 留下的额外空白是一个非常顽固的问题。你不能通过调用fflush 来便携地做到这一点。你不能通过调用rewind 来便携地做到这一点。如果您关心正确、可移植的代码,您基本上有三种选择:
- 编写您自己的代码以显式读取和丢弃“额外”字符(通常,直到并包括下一个换行符)。
- 不要试图混用
scanf 和其他电话。不要拨打scanf,然后再尝试拨打getchar 或fgets。如果您调用scanf,然后稍后使用缺少“秘密额外功能”的指令之一(例如"%c")调用scanf,请在格式说明符之前插入一个额外的空格以导致空格被跳过. (也就是说,使用" %c" 而不是"%c"。)
- 根本不要使用
scanf — 以fgets 或getchar 的形式全部执行您的输入。
另见What can I use for input conversion instead of scanf?
附录:scanf 对空格的处理通常看起来令人费解。如果上述解释还不够充分,那么查看一些详细说明 scanf 内部工作原理的实际 C 代码可能会有所帮助。 (我要展示的代码显然不是您系统实现背后的确切代码,但它会是相似的。)
当scanf 处理%d 指令时,您可能会想象它会做这样的事情。 (预先警告:我要向您展示的第一段代码是不完整的。我需要尝试三次才能正确。)
c = getchar();
if(isdigit(c)) {
int intval;
intval = c - '0';
while(isdigit(c = getchar())) {
intval = 10 * intval + (c - '0');
}
*next_pointer_arg = intval;
n_vals_converted++;
} else {
/* saw no digit; processing has failed */
return n_vals_converted;
}
让我们确保我们了解这里发生的一切。我们被告知要处理%d 指令。我们通过调用getchar() 从输入中读取一个字符。如果该字符是数字,则它可能是构成整数的几个数字中的第一个。我们读取字符,只要它们是数字,我们就将它们添加到整数值intval,我们正在收集。转换包括减去常量'0',将 ASCII 字符代码转换为数字值,然后连续乘以 10。一旦我们看到一个不是数字的字符,我们就完成了。我们将转换后的值存储到调用者传递给我们的指针中(这里示意性地但大致由指针值next_pointer_arg 表示),我们将一个变量添加到变量n_vals_converted 中,以记录我们成功扫描了多少个值并转换,最终将是scanf的返回值。
另一方面,如果我们甚至没有看到一个数字字符,我们就失败了:我们立即返回,我们的返回值是到目前为止我们成功扫描和转换的值的数量(可能最好是 0)。
但实际上这里有一个微妙的错误。假设输入流包含
"123x"
此代码将成功扫描并将数字1、2和3转换为整数123,并将此值存储到*next_pointer_arg中。 但是,它将读取字符x,并且在循环while(isdigit(c = getchar())) 中对isdigit 的调用失败后,x 字符将被有效地丢弃:它不再是在输入流上。
scanf 的规范表明它不应该这样做。 scanf 的规范说未解析的字符应该留在输入流中。如果用户实际上传递了格式说明符"%dx",这意味着,在读取和解析一个整数之后,输入流中需要一个文字x,而scanf将不得不显式读取和匹配那个角色。所以在解析%d指令的过程中不会误读并丢弃x。
所以我们需要稍微修改我们假设的%d 代码。每当我们读取一个结果不是整数的字符时,我们必须按字面意思将它放回输入流,以供其他人稍后读取。实际上<stdio.h> 中有一个函数可以做到这一点,与getc 正好相反,称为ungetc。这是代码的修改版本:
c = getchar();
if(isdigit(c)) {
int intval;
intval = c - '0';
while(isdigit(c = getchar())) {
intval = 10 * intval + (c - '0');
}
ungetc(c, stdin); /* push non-digit character back onto input stream */
*next_pointer_arg = intval;
n_vals_converted++;
} else {
/* saw no digit; processing has failed */
ungetc(c, stdin);
return n_vals_converted;
}
你会注意到我在代码的两个地方添加了两个对ungetc的调用,在调用getchar和isdigit之后,代码刚刚发现它读取了一个字符不是一个数字。
阅读一个字符然后改变主意似乎很奇怪,这意味着您必须“未阅读”它。在不阅读的情况下查看即将出现的字符(以确定它是否是数字)可能更有意义。或者,在读取一个字符并发现它不是数字后,如果要处理该字符的下一段代码就在scanf 中,那么将其保存在局部变量c 中可能是有意义的,而不是调用ungetc 将其推回输入流,然后再调用getchar 再次从输入流中获取它。但是,在提到了其他两种可能性之后,我只想说,现在,我将继续使用使用 ungetc 的示例。
到目前为止,我已经展示了您可能想象的 scanf 处理 %d 背后的代码。但是到目前为止我展示的代码仍然很不完整,因为它没有显示“秘密额外的力量”。它立即开始寻找数字字符;它不会跳过前导空格。
那么,这是我的第三个也是最后一个示例片段 %d-processing 代码:
/* skip leading whitespace */
while(isspace(c = getchar())) {
/* discard */
}
if(isdigit(c)) {
int intval;
intval = c - '0';
while(isdigit(c = getchar())) {
intval = 10 * intval + (c - '0');
}
ungetc(c, stdin); /* push non-digit character back onto input stream */
*next_pointer_arg = intval;
n_vals_converted++;
} else {
/* saw no digit; processing has failed */
ungetc(c, stdin);
return n_vals_converted;
}
只要是空白字符,初始循环就会读取并丢弃字符。它的形式与后面的循环非常相似,只要它们是数字就读取和处理字符。初始循环将读取比看起来应该多一个字符:当isspace 调用失败时,这意味着它刚刚读取了一个 非 空白字符。不过没关系,因为我们正要读取一个字符,看看它是否是第一个数字。
[脚注:这段代码还远非完美。一个非常重要的问题是它在解析过程中没有对 EOF 进行任何检查。另一个问题是它不会在数字前查找- 或+,因此它不会处理负数。另一个更晦涩的问题是,具有讽刺意味的是,像isdigit(c) 这样看起来很明显的调用并不总是正确的——strictly speaking 它们需要稍微麻烦地呈现为isdigit((unsigned char)c)。]
如果你还和我在一起,我的意思是用具体的方式说明这两点: