【问题标题】:How does scanf know if it should scan a new value?scanf 如何知道它是否应该扫描一个新值?
【发布时间】:2021-10-27 08:16:26
【问题描述】:

我正在研究scanf 的工作原理。

在扫描其他类型变量后,char 变量存储getchar()scanf("%c") 的空格('\n')。 为了防止这种情况,他们应该清除缓冲区。我用rewind(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);
}

【问题讨论】:

  • 你说得对,空格处理是理解和使用scanf 的一个大问题,但你有一个根本问题:rewind() 确实清除空格。
  • 你用的是什么系统? Linux?视窗? stdin-&gt;_Placeholder 中的值意味着什么的假设从何而来?
  • _Placeholder 是您的系统实现 &lt;stdio.h&gt; 的唯一内部变量。我们不知道它的作用或含义,而且我们不太可能通过研究它学到任何有用的东西——我们不太可能准确学到任何东西。
  • @KamilCuk 我在 Windows 上使用 vs2019,我在 ctrl+单击类型关键字 FILE 中找到了有关 _Placeholder 的信息。我只是认为_Placeholder 是file position indicator。不是吗?
  • 我建议你永远不要使用 scanf。请改用 fgets 或 fread 和 sscanf。不要同时阅读和解释你阅读的内容。先阅读,再解读所读内容。

标签: c scanf stdin rewind


【解决方案1】:

你至少有三个误解:

  • “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 指令中的第一个。它扫描字符12,将它们转换为整数12 并将其存储在变量a 中。它在它看到的第一个非数字字符处停止读取,这是23 之间的空格字符。输入流现在是

" 34\n"

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

scanf 接下来处理第二个%d 指令。它不会立即找到数字字符,因为空格字符仍然存在。但这没关系,因为像大多数(但不是全部)scanf 格式指令一样,%d 有一个秘密的额外功能:它会在读取和转换整数之前自动跳过空白字符。所以第二个%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 的另一个调用,并且如果它涉及(许多)格式说明符之一,该格式说明符具有跳过空格的秘密额外功能,则该格式说明符将跳过换行符,然后执行扫描和转换换行符之后的任何输入的工作,程序将按您的预期运行。

  • 但是如果下一个输入调用不是scanf的调用,或者如果它是对scanf的调用,它涉及为数不多的没有秘密额外权力的输入说明符之一,换行将被“跳过”;它将被读取为实际输入。如果下一个输入调用是getchar,它将读取并返回换行符。如果下一个输入调用是fgets,它将读取并返回一个空行。如果下一个输入调用是带有%c 指令的scanf,它将读取并返回换行符。如果下一个输入调用是带有%[^\n] 指令的scanf,它将读取一个空行。 (实际上,%[^\n] 在这种情况下会读取 nothing,因为它会将 \n 留在输入中。)

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

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

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

另见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"

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

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

所以我们需要稍微修改我们假设的%d 代码。每当我们读取一个结果不是整数的字符时,我们必须按字面意思将它放回输入流,以供其他人稍后读取。实际上&lt;stdio.h&gt; 中有一个函数可以做到这一点,与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的调用,在调用getcharisdigit之后,代码刚刚发现它读取了一个字符不是一个数字。

阅读一个字符然后改变主意似乎很奇怪,这意味着您必须“未阅读”它。在不阅读的情况下查看即将出现的字符(以确定它是否是数字)可能更有意义。或者,在读取一个字符并发现它不是数字后,如果要处理该字符的下一段代码就在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)。]

如果你还和我在一起,我的意思是用具体的方式说明这两点:

  • %d 能够自动跳过前导空格的原因是 (a) 规范说明它应该这样做,并且 (b) 它有明确的代码可以这样做,正如我的第三个示例所示。

  • scanf 总是在输入流上留下未处理的输入(即,在它确实读取和处理的输入之后的输入)是因为 (a) 再次,规范说它是应该和 (b) 它的代码通常会显式调用ungetc 或等效的,以确保每个未处理的字符都保留在输入中,如我的第二个示例所示。

【讨论】:

  • 感谢您的回答!
  • “如何”是什么意思,@suraj?正如史蒂夫解释的那样,这是scanf 在处理%d 指令时跳过(意味着读取和丢弃)所有前导空格(如果有)的行为的一部分。这是完全标准的并且有据可查。 scanf 做到了,但除此之外,细节未指定。
  • @Suraj %d 知道如何跳过空格 因为它被定义(必需)到。 为什么跳过的空格在缓冲区中不可用? 不确定您的意思。如果你输入了空格,它就在缓冲区中。 %d 将消耗并丢弃前导空格(在数字之前)。 %d 不会消耗或丢弃数字后的空格。 我们只有最后一个空格字符不知道你的意思。 %d 之前的空格是如何丢失的? 因为scanf 在处理%d 指令的任务中主动读取并丢弃这些字符。
  • @Suraj 另请参阅我扩展答案中的“附录”。
  • @chux-ReinstateMonica nun-numeric character 是的,这是更好理解的好主意。
【解决方案2】:

你的方法有一些问题:

  • 您使用了 FILE 对象 _Placeholder 的未记录的特定于实现的成员,该成员可能在不同平台上可用,也可能不可用,并且其内容是特定于实现的。
  • 您使用scanf_s(),它是Microsoft 特定的所谓安全 版本的scanf():此功能是可选的,可能并非在所有平台上都可用。此外,Microsoft 的实现不符合 C 标准:例如,在 &amp;ch 之后传递的 size 参数在 VS 中记录为 UINT 类型,而 C 标准将其指定为 size_t,在 64 位Windows 版本的大小不同。

scanf() 使用起来非常棘手:即使是经验丰富的 C 程序员也会被它的许多怪癖和陷阱所困扰。在您的代码中,您测试了 %d%c,它们的行为非常不同:

  • 对于%dscanf() 将首先读取并丢弃任何空白字符,例如空格、TAB 和换行符,然后读取可选符号+-,然后它期望读取至少一个digit 并在它得到一个不是数字的字节时停止,并将这个字节留在输入流中,用ungetc() 或等效项将其推回。如果无法读取任何数字,则转换失败,第一个非数字字符在输入流中处于待处理状态,但之前的字节不一定被推回。
  • 处理%c 要简单得多:读取单个字节并将其存储到char 对象中,否则如果流位于文件末尾,则转换失败。

如果输入流绑定到终端,则在%d 之后处理%c 会很棘手,因为用户将在%d 的预期数字之后输入换行符,并且将立即为%c 读取此换行符。通过在格式字符串中的%c 之前插入一个空格,程序可以忽略%c 预期字节之前的空格:res = scanf(" %c", &amp;ch);

为了更好地理解scanf() 的行为,您应该输出每个调用的返回值和流当前位置,通过ftell() 获得。首先将流设置为二进制模式,以使ftell() 的返回值与文件开头的字节数完全相同,也更可靠。

这是修改后的版本:

#include <stdio.h>

#ifdef _MSC_VER
#include <fcntl.h>
#include <io.h>
#endif

int main() {
    int x, res;
    char ch;
    long A, B, C, D;

#ifdef _MSC_VER
    _setmode(_fileno(stdin), _O_BINARY);
#endif

    A = ftell(stdin);
    printf("A : %ld\n", A);

    x = 0;
    res = scanf_s("%d", &x);

    B = ftell(stdin);
    printf("B : %ld, res=%d, x=%d\n", B, res, x);

    rewind(stdin);
    C = ftell(stdin);
    printf("C : %ld\n", C);

    ch = 0;
    res = scanf_s("%c", &ch, 1);
    D = ftell(stdin);
    printf("D : %ld, res=%d, ch=%d (%c)\n", D, res, ch, ch);

    return 0;
}

【讨论】:

  • 感谢您的回答!
【解决方案3】:

这里有一些代码说明了%d 转换说明符的行为;它可能有助于理解scanf 的这一方面是如何工作的。这不是它在任何地方实际实现的方式,但它遵循相同的规则(已更新以处理前导 +/- 符号、检查溢出等)。

#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>

/**
 * Mimics the behavior of the scanf %d conversion specifier.
 * Skips over leading whitespace, then reads and converts
 * decimal digits up to the next non-digit character.
 *
 * Returns EOF if no non-whitespace characters are
 * seen before EOF.
 *
 * Returns 0 if the first non-whitespace character
 * is not a digit.
 *
 * Returns 1 if at least one decimal digit was
 * read and converted.
 *
 * Stops reading on the first non-digit
 * character, pushes that character back
 * on the input stream.
 *
 * In the event of a signed integer overflow,
 * sets errno to ERANGE.
 */
int scan_to_int( FILE *stream, int *value )
{
  int conv = 0;
  int tmp = 0;
  int c;
  int sign = 0;

  /**
   * Skip over leading whitespace
   */
  while( ( c = fgetc( stream ) ) != EOF && isspace( c ) )
    ; // empty loop

  /**
   * If we see end of file before any non-whitespace characters,
   * return EOF.
   */
  if ( c == EOF )
    return c;

  /**
   * Account for a leading sign character.
   */
  if ( c == '-' || c == '+' )
  {
    sign = c;
    c = fgetc( stream );
  }

  /**
   * As long as we see decimal digits, read and convert them
   * to an integer value.  We store the value to a temporary
   * variable until we're done converting - we don't want
   * to update value unless we know the operation was
   * successful
   */
  while( c != EOF && isdigit( c ) )
  {
    /**
     * Check for overflow.  While setting errno on overflow
     * isn't required by the C language definition, I'm adding
     * it anyway.  
     */
    if ( tmp > INT_MAX / 10 - (c - '0') )
      errno = ERANGE;

    tmp = tmp * 10 + (c - '0');
    conv = 1;
    c = fgetc( stream );
  }

  /**
   * Push the last character read back onto the input
   * stream.
   */
  if ( c != EOF )
    ungetc( c, stream );

  /**
   * If we read a sign character (+ or -) but did not have a
   * successful conversion, then that character was not part
   * of a numeric string and we need to put it back on the
   * input stream in case it's part of a non-numeric input.
   */
  if ( sign && !conv )
    ungetc( sign, stream );

  /**
   * If there was a successful read and conversion,
   * update the output parameter.
   */
  if ( conv )
    *value = tmp * (sign == '-' ? -1 : 1);

  /**
   * Return 1 if the read was successful, 0 if there
   * were no digits in the input. 
   */
  return conv;
}

/**
 * Simple test program - attempts to read 1 integer from
 * standard input and display it.  Display any trailing
 * characters in the input stream up to and including
 * the next newline character.
 */
int main( void )
{
  int val;
  int r;

  errno = 0;

  /**
   * Read the next item from standard input and
   * attempt to convert it to an integer value.
   */
  if ( (r = scan_to_int( stdin, &val )) != 1 )
    printf( "Failed to read input, r = %d\n", r );
  else
    printf( "Read %d%s\n", val, errno == ERANGE ? " (overflow)" : "" );

  /**
   * If we didn't hit EOF, display the remaining
   * contents of the input stream.
   */
  if ( r != EOF )
  {
    fputs( "Remainder of input stream: {", stdout );
    int c;
    do {
      c = fgetc( stdin );
      switch( c )
      {
        case '\a': fputs( "\\a", stdout ); break;
        case '\b': fputs( "\\b", stdout ); break;
        case '\f': fputs( "\\f", stdout ); break;
        case '\n': fputs( "\\n", stdout ); break;
        case '\r': fputs( "\\r", stdout ); break;
        case '\t': fputs( "\\t", stdout ); break;
        default: fputc( c, stdout ); break;
      }
    } while( c != '\n' );

    fputs( "}\n", stdout );
  }

  return 0;
}

一些例子 - 首先,我们发出 EOF 信号(在我的例子中,通过键入 Ctrl-D):

$ ./convert 
Failed to read input, r = -1

接下来,我们传入一个非数字字符串:

$ ./convert 
abcd
Failed to read input, r = 0
Remainder of input stream: {abcd\n}

由于没有进行任何转换,输入流的其余部分包含我们键入的所有内容(包括按 Enter 的换行符)。

接下来,一个带有非数字尾随字符的数字字符串:

$ ./convert 
12cd45
Read 12
Remainder of input stream: {cd45\n}

我们在 'c' 处停止读取 - 只有前导的 12 被读取和转换。

由空格分隔的几个数字字符串 - 仅转换第一个字符串:

$ ./convert 
123 456 789
Read 123
Remainder of input stream: {\t456\t789\n}

还有一个带有前导空格的数字字符串:

$ ./convert 
      12345
Read 12345
Remainder of input stream: {\n}

处理前导标志:

$ ./convert 
-123abd
Read -123
Remainder of input stream: {abd\n}

$ ./convert 
    +456
Read 456
Remainder of input stream: {\n}

$ ./convert 
-abcd
Failed to read input, r = 0
Remainder of input stream: {-abcd\n}

最后,我们添加了溢出检查 - 请注意,C 语言标准不需要 scanf 来检查溢出,但我认为这样做很有用:

$ ./convert 
123456789012345678990012345667890
Read -701837006 (overflow)

输入流的剩余部分:{\n} %d%i%f%s 等都跳过前导空格,因为空格在这些情况下没有意义,除非用作输入之间的分隔符。 %c%[ 跳过前导空格,因为它可能对那些特定的转换有意义(有时你想要 > 知道你刚才读到的字符是空格、制表符还是换行符)。

正如 Steve 所指出的,C stdio 例程中的空白处理一直是一个棘手的问题,没有一种解决方案总是效果最好,尤其是因为不同的库例程处理它的方式不同。

【讨论】:

  • 次要:c != EOF &amp;&amp; isdigit( c ) 可替换为 isdigit( c )
  • "模仿 scanf %d 的行为" --> 除了前面的 sign 字符是不允许的。
  • @chux-ReinstateMonica:我会修复它,但我太懒了。
  • UV 使用良好的工程效率 - 并非所有问题都需要修复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-06
  • 1970-01-01
  • 2011-03-20
  • 2014-09-19
  • 2011-04-03
  • 1970-01-01
相关资源
最近更新 更多