scanf 如何知道它是否应该扫描一个新值?

我正在研究如何scanf工作。

扫描其他类型变量后,char 变量通过getchar()or存储一个空格('\n')scanf("%c")。为了防止这种情况,他们应该清除缓冲区。我做到了rewind(stdin)

尽管 stdin 被倒带,但先前的输入值仍保留在缓冲区中。我可以正常使用以前的值做一些事情。(没有运行时错误)但是如果我再试scanf一次,即使缓冲区中有一个正常值,scanf 也会扫描一个新值。scanf 如何确定是否应该扫描新值?

我用下面的代码找到了这个机制。

#include <stdio.h>
#define p stdin

int main() {
    int x;
    char ch;

    void* A, * B, * C, * D, * E;

    A = p->_Placeholder;
    printf("A : %p\n", A);//first time, it shows 0000
    scanf_s("%d", &x);

    B = p->_Placeholder;
    printf("B : %p\n", B);//after scanned something, I think it's begin point of buffer which is assigned for this process
    rewind(stdin);//rewind _Placeholder 

    C = p->_Placeholder;
    printf("C : %p\n", C);//it outputs the same value as B - length of x

    D = p->_Placeholder;
    printf("D : %c\n", ((char*)D)[0]);//the previous input value is printed successfully without runtime error. it means buffer is not be cleared by scanf
    scanf_s("%c", &ch, 1);//BUT scanf knows the _Placeholder is not pointing new input value, so it will scan a new value from console. How??

    E = p->_Placeholder;
    printf("E : %p\n", E);
    printf("ch : %c\n", ch);
}

回答

你至少有三个误解:

  • “char 变量存储一个空格”
  • rewind(stdin) 清除缓冲区
  • _Placeholder告诉你一些关于如何scanf处理空格的有趣事情

但是,对不起,这些都不是真的。

让我们回顾一下scanf 实际如何处理空格。我们从两个重要的背景信息开始:

  • 换行符\n在大多数情况下是一个普通的空白字符。它像任何其他字符一样占用输入缓冲区中的空间。当您按下 Enter 键时,它会到达输入缓冲区。
  • 当它完成解析 -%指令时,scanf总是在输入流上留下未解析的输入。

假设你写

int a, b;
scanf("%d%d", &a, &b);

假设您运行该代码并键入,作为输入

12 34

然后按 Enter 键。发生什么了?

首先,输入流 ( stdin) 现在包含六个字符:

"12 34\n"

scanffirst 处理%d你给它的两个指令中的第一个。它扫描字符12,将它们转换为整数 12 并将其存储在变量中a。它在它看到的第一个非数字字符(即2和之间的空格字符)处停止读取3。输入流现在是

" 34\n"

请注意,空格字符仍在输入流中。

scanfnext 处理第二个%d指令。它不会立即找到数字字符,因为空格字符仍然存在。但这没关系,因为像大多数(但不是全部)scanf格式指令一样,%d有一个秘密的额外功能:它在读取和转换 integer 之前自动跳过空白字符。因此,第二%d读出并丢弃空格字符,然后读取字符34在变量和将它们转换为整数34,它存储b

现在scanf完成了。输入流只包含换行符:

"\n"

接下来,让我们看一个略有不同的例子——尽管正如我们将看到的,实际上非常相似——例子。假设你写

int x, y;
scanf("%d", &x);
scanf("%d", &y);

假设您运行该代码并键入,作为输入

56
78

(这是在两行上,这意味着您按 Enter 两次)。现在会发生什么?

在这种情况下,输入流最终将包含以下六个字符:

"56\n78\n"

第一个scanf调用有一个%d要处理的指令。它扫描字符56,将它们转换为整数 56 并将其存储在变量中x。它在它看到的第一个非数字字符处停止读取,即6. 输入流现在是

"\n78\n"

请注意,换行符(两个换行符)仍在输入流中。

现在第二个scanf调用运行。它也有一个%d要处理的指令。输入流中的第一个字符不是数字:它是一个换行符。但这没关系,因为%d知道如何跳过空格。因此,它读取并丢弃换行符,然后读取字符78并将它们转换为整数78,在可变其存储y

现在第二个scanf完成了。输入流只包含换行符:

"\n"

这一切可能都是有道理的,可能看起来并不奇怪,可能让你觉得,“好吧,那有什么大不了的?” 最重要的是:在两个示例中,输入都包含最后一个换行符

假设,稍后在您的程序中,您有一些其他输入要读取。我们现在来到一个非常重要的决策点:

在最后一种情况下,“额外”空格会导致问题。在最后一种情况下,您发现自己需要明确地“刷新”或丢弃多余的空格。

没有陷入所有血腥细节的泥潭,事实证明,清除或丢弃留下的额外空白scanf是一个非常顽固的问题。你不能通过调用来便携地做到这一点fflush。你不能通过调用来便携地做到这一点rewind。如果你关心正确的、可移植的代码,你基本上有三种选择:

  1. 编写您自己的代码来显式读取和丢弃“额外”字符(通常,直到并包括下一个换行符)。
  2. 不要试图scanf与其他电话混在一起。不要调用scanf,然后,稍后再尝试调用getcharfgets。如果您调用scanf,然后scanf使用"%c"缺少“秘密额外功能”的指令之一(例如)调用,请在格式说明符之前插入一个额外的空格以导致跳过空格。(也就是说,使用" %c"代替"%c"。)
  3. 根本不要使用scanf—以或 的形式进行所有输入。fgetsgetchar

另请参阅我可以使用什么代替 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"

此代码将成功地扫描和数字转换12以及3为整数123,并存储该数值到*next_pointer_arg但是,它将读取字符x,并且在isdigit循环调用while(isdigit(c = getchar()))失败后,x字符将被有效地丢弃:它不再在输入流中。

的规范scanf说它应该这样做。的规范scanf说未解析的字符应该留在输入流上。如果用户实际上已经传递了格式说明符"%dx",那就意味着,在读取和解析一个整数之后,x输入流中需要一个文字,并且scanf必须显式读取和匹配该字符。所以它不会x在解析%d指令的过程中意外读取和丢弃。

所以我们需要%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和 then 之后isdigit,代码刚刚发现它读取了一个不是数字的字符。

阅读一个字符然后改变主意似乎很奇怪,这意味着您必须“未读”它。在不阅读即将出现的字符(以确定它是否为数字)的情况下查看它可能更有意义。或者看了一个字符,发现这不是一个数字,如果下一个代码块,那将处理该字符是正确的在这里scanf,它可能是有意义的只是不停地在局部变量c,而不是要求ungetc其推返回输入流,然后再次调用getchar以从输入流中获取它。但是,在提到这其他两种可能性之后,我只想说,现在,我将继续使用使用getc.

到目前为止,我已经展示了您可能想象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 进行任何检查。另一个问题是它不查找-+之前的数字,所以它不会处理负数。另一个更模糊的问题是,具有讽刺意味的是,看起来很明显的调用 likeisdigit(c)并不总是正确的 -严格来说,它们需要稍微麻烦地呈现为isdigit((unsigned char)c).]

如果你还和我在一起,我在这一切的观点就是以具体的方式说明这两点:


以上是scanf 如何知道它是否应该扫描一个新值?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>