scanf()会将换行符留在输入缓冲区中。

133

我有以下程序:

int main(int argc, char *argv[])
{
    int a, b;
    char c1, c2;
    printf("Enter something: ");
    scanf("%d", &a); // line 1
    printf("Enter other something: ");
    scanf("%d", &b); // line 2

    printf("Enter a char: ");
    scanf("%c", &c1); // line 3
    printf("Enter another char: ");
    scanf("%c", &c2); // line 4

    printf("Done"); // line 5

    system("PAUSE");

    return 0;
}

根据我在C语言书籍中的阅读,作者说scanf()会在缓冲区中留下一个换行符,因此,程序不会在第4行停止等待用户输入数据,而是将换行符存储在c2中并移动到第5行。

这是对的吗?

但是,这只会发生在char数据类型中吗?因为我在第1、2、3行使用int数据类型时没有看到这个问题。这是正确的吗?


1
有时建议在调用 scanf() 之前使用 fflush(stdin) 来读取单个字符。请阅读使用 fflush(stdin),了解该方法的优缺点和替代方法的讨论(这在Windows上或多或少有效,在大多数其他地方则无效)。 - Jonathan Leffler
请问您指的是哪本书? - surya kiran
1
@JonathanLeffler 使用 fflush(stdin) 是明显的未定义行为 - Zakk
7个回答

123

scanf()函数在尝试解析字符以外的转换之前会自动跳过前导空格。 字符格式(主要是%c;还有扫描集%[…]%n)是例外,它们不跳过空格。

使用带有前导空格的" %c"来跳过可选的空格。 在scanf()格式字符串中不要使用尾随空格。

请注意,这仍然不消耗输入流中留下的任何尾随空格,即使是到行末,所以如果还使用getchar()fgets()读取同一个输入流时要小心。 我们只是让scanf在转换之前跳过空白,就像对于%d和其他非字符转换一样。


请注意,除了转换之外的非空格“指令”(使用POSIX scanf术语)(例如scanf("order = %d", &order);中的文本文字)也不会跳过空格。 文字order必须与下一个要读取的字符匹配。

因此,如果您想跳过前一行的换行符但仍需要在固定字符串上执行文本匹配,那么您可能需要使用" order = %d",就像此问题中那样。


15
%c%n%[] 是三个指定的格式化字符,它们不会读取前导空格。 - chux - Reinstate Monica
@chux 在其他情况下,scanf函数会清除缓冲区中输入之前的所有空格,或者忽略它们,但它们仍然存在吗? - Suraj Jain
@SurajJain 是的。 - chux - Reinstate Monica
5
参见Trailing blank in scanf() format stringscanf() asking twice for input while I expect it to ask only once,讨论了格式字符串中的尾随空格。它们是一个坏主意——如果你期望人类交互,那么这个主意就异常糟糕,也对程序交互有影响。 - Jonathan Leffler
这个答案非常好地解释了scanf()的读取和格式化,而不仅仅是按照格式读取。 - Priyanshul Govil

42
使用scanf(" %c", &c2);。这将解决您的问题。

14

另一个选项(我从这里得到)是通过使用赋值抑制选项读取并丢弃换行符。为此,我们只需在%c之间放置带有星号的格式来读取字符:

scanf("%d%*c",&a); // line 1
scanf("%c%*c",&c1); // line 3

scanf 读取下一个字符(即换行符),但不将其分配给任何指针。

然而,最终我赞同 FAQ 的最后一种选择:

  

或者,根据您的要求,您也可以忘记 scanf()/getchar(),使用 fgets() 从用户获取一行文本并自己解析。


6
这种技术的问题在于,如果用户在输入时键入 a、空格和换行符,则被抑制的字符转换会读取空格并仍保留换行符。如果用户在期望输入 a 时键入了“超级特别棒的”单词,则需要处理很多额外字符。此外,你永远无法确定末尾的抑制转换是否成功,因为它们不会被计入 scanf() 的返回值中。 - Jonathan Leffler
3
仅会忽略一个字符。在“%”前添加一个空格将会忽略任意数量的前导空格(包括没有)。 - Weather Vane

10
为了回应我在另一个关于C ++的答案中发表的观点:我建议放弃scanf(),永远不要使用它,而是使用fgets()sscanf()
原因是,在类Unix系统中,默认情况下,CLI程序运行的终端会在您的程序看到之前对用户输入进行一些处理。它会缓冲输入直到输入换行符,并允许进行一些基本的行编辑,例如使退格键起作用。
因此,您永远无法一次获取单个字符或几个单个字符,只能获取整行。但是这并不是例如scanf(“%d”)所处理的内容,它仅处理数字,然后停止,将其余部分缓存在C库中,供未来的stdio函数使用。如果您的程序具有例如
printf("Enter a number: ");
scanf("%d", &a);

printf("Enter a word: ");
scanf("%s", word);

当你输入行 123 abcd 时,它会在换行符给出后同时完成两个 scanf(),但第一个 scanf() 不会在用户按下空格时返回,即使这是数字结束的地方(因为此时该行仍在终端的行缓冲区中);第二个 scanf() 不会等待您再输入一行(因为输入缓冲区已经包含足以填充 %s 转换的内容)。
这不是用户通常期望的!
相反,他们希望按下回车键完成输入,如果您按下回车键,则会得到默认值或错误提示,可能建议您真正提供答案。
您无法使用 scanf("%d") 实现这一点。如果用户只按回车键,什么也不会发生。因为 scanf() 仍在等待数字。终端将该行发送到程序,但您的程序看不到它,因为 scanf() 已经吃掉了它。您没有机会对用户的错误做出反应。
这也不是很有用。
因此,我建议使用 fgets()getline() 一次读取完整的输入行。这与终端给出的完全匹配,并且始终在用户输入一行后给您的程序控制权。您对输入行的处理取决于您,如果您想要一个数字,则可以使用 atoi()strtol() 或甚至 sscanf(buf, "%d", &a) 来解析数字。 sscanf() 没有与 scanf() 相同的不匹配,因为它读取的缓冲区大小是有限的,当它结束时,它就结束了——该函数无法等待更多内容。 (对于支持如何跨越换行符等空格的文件格式,常规文件上的 fscanf() 也可以正常工作。对于面向行的数据,我仍然会使用 fgets()sscanf()。)

因此,不要使用我之前的内容,而是使用类似于以下内容:

printf("Enter a number: ");

fgets(buf, bufsize, stdin);
sscanf(buf, "%d", &a);

或者,实际上,还要检查sscanf()的返回值,这样您就可以检测空行和其他无效数据:

#include <stdio.h>

int main(void)
{
    const int bufsize = 100;
    char buf[bufsize];
    int a;
    int ret;
    char word[bufsize];

    printf("Enter a number: ");
    fgets(buf, bufsize, stdin);

    ret = sscanf(buf, "%d", &a);

    if (ret != 1) {
        fprintf(stderr, "Ok, you don't have to.\n");
        return 1;
    }

    printf("Enter a word: ");
    fgets(buf, bufsize, stdin);

    ret = sscanf(buf, "%s", word);
    if (ret != 1) {
        fprintf(stderr, "You make me sad.\n");
        return 1;
    }

    printf("You entered %d and %s\n", a, word);
}

当然,如果你想让程序坚持下去,你可以创建一个简单的函数来循环使用fgets()sscanf(),直到用户愿意按照指示操作;或者立即退出并显示错误。这取决于你认为如果用户不想跟着玩,你的程序应该怎么做。
你可以做类似的事情,例如循环使用getchar()读取字符直到换行符,在scanf("%d")返回后清除缓冲区中留下的任何垃圾,但这不能解决用户在空行上只按回车键的情况。无论如何,fgets()将读取直到换行符,所以你不必自己处理。

1
fgets()/getline() 然后使用 sscanf()strtol() 等函数,相比直接使用 scanf(),还有另一个巨大的优势。当程序遇到意外输入时,输入流不会处于未知状态,从而避免了数据丢失的情况下无法恢复的问题。 - Andrew Henle
有关scanf的替代方法,可以参考以下阅读材料:除scanf外,我还能用什么进行输入转换? 此外,这份指南也可能对您有所帮助:远离scanf()的初学者指南 - Andreas Wenzel

8
在调用第二个scanf()之前,请使用getchar()
scanf("%c", &c1);
getchar();  // <== remove newline
scanf("%c", &c2);

2
只要用户没有输入其他的内容,比如尾随空格,这个方法就可行。但它不如扫描到下一个换行符的循环好:int c; while ((c = getchar()) != EOF && c != '\n') ;(如果不是在注释中,则写成三行)。它通常已经足够了;它并不是万无一失的(你必须记住,愚蠢的人很擅长让事情崩溃)。 - Jonathan Leffler
@JonathanLeffler 你的意思是代码要写在两行上吗?int c; while ((c = getchar()) != EOF && c != '\n') ; - john
3
@john:编译器无所谓循环语句是一行、两行还是三行或更多。如果由我自己决定,那就会是三行:变量声明、循环控制和循环体。分号表示空的循环体应该独占一行以强调其有意义(正如K&R建议的那样)。 - Jonathan Leffler

0

有两种解决方法。其中一个是巧妙地捕捉额外的空白。

getchar(); // (1) add `getchar()`
scanf("%c", &c1);

scanf(" %c", &c1); // (2) add whitespace before %c

另一种方法是使用 fgets() 替代 scanf()。注意:gets() 是不安全的,请参见 为什么 gets 是危险的
相比之下,我更推荐第二种方式。因为它更易读和易于维护。例如,在第一种方式中,您必须在添加/移动某些 scanf() 之前考虑一段时间。

-4
/*Take char input using scanf after int input using scanf just use fflush(stdin) function  after int input */
#include<stdio.h>
#include<conio.h>
void main()
{
  int x;
  char y;
  clrscr();
  printf(" enter an int ");
  scanf("%d",&x);
  fflush(stdin);
  printf("\n Now enter a char");
  scanf("%c",&y);
  printf("\n X=%d and Y=%c",x,y);
  getch();
}

2
请注意关于使用fflush(stdin)的注意事项。 - Jonathan Leffler
1
conio.h 不是 ISO C 的一部分,而是一个特定于平台的头文件。函数 clrscrgetch 也是特定于平台的。请注意,该问题没有标记任何特定的平台,因此该问题不适用于任何特定的平台。因此,如果您决定提供特定于平台的答案,您应该至少清楚地标记它,并指定您的答案适用于哪个平台。 - Andreas Wenzel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接