scanf: "%[^\n]" 跳过了第二个输入,但是 " %[^\n]" 却没有。为什么?

29

考虑以下代码:

#include <stdio.h>

int main (void)
{
  char str1[128], str2[128], str3[128];

  printf ("\nEnter str1: ");
  scanf ("%[^\n]", str1);
  printf ("\nstr1 = %s", str1);

  printf ("\nEnter str2: ");
  scanf ("%[^\n]", str2);
  printf ("\nstr2 = %s", str2);

  printf ("\nEnter str3: ");
  scanf ("%[^\n]", str3);
  printf ("\nstr3 = %s", str3);

  printf ("\n");
  return 0;
}

当程序执行时,只有第一个scanf会停下等待用户输入。程序不会为后面的scanf停下来等待用户输入。但是,如果将格式字符串从"%[^\n]"更改为" %[^\n]"(注意%前面的空格),那么它就可以正常工作了。是否自动接受了先前输入缓冲区中的某个现有换行符?但清除stdin并不能解决这个问题。

这是什么原因。


4
如果您使用fgets,您的生活将变得更加轻松。我通常建议在大多数情况下避免使用scanf,但特别是在这种情况下,您正在为一个非常简单的任务使用一个非常强大且难以使用的函数。 - Chris Lutz
没问题,我这边可以输入字符串,只是对这个特性的行为有些好奇。 - phoxis
6个回答

44

在你读取所需内容后,只需要“消耗”掉'\n'字符即可。使用以下格式指令:

"%[^\n]%*c"

以下代码会将输入流中直到遇到换行符的所有字符读入字符串中,并跳过一个单独的换行符('*'代表“赋值抑制”)。

否则,换行符会留在输入流中等待立即终止随后的 "%[^\n]" 格式指令。

将空格字符添加到格式指令中 (" %[^\n]") 的问题是这个空格会匹配 任何 空白字符。因此,它会吃掉前面输入的换行符,但也会吃掉任何其他空白字符(包括多个换行符)。

针对你的示例进行更新:

  char* fmt = "%[^\n]%*c";

  printf ("\nEnter str1: ");
  scanf (fmt, str1);
  printf ("\nstr1 = %s", str1);

  printf ("\nEnter str2: ");
  scanf (fmt, str2);
  printf ("\nstr2 = %s", str2);

  printf ("\nEnter str3: ");
  scanf (fmt, str3);
  printf ("\nstr2 = %s", str3);

  printf ("\n");

赋值抑制是一个很好的解决方案,实际上这是我第一次看到它被用在某些好的地方。 - phoxis
...<headdesk>我刚刚花了一个月的时间使用“%*1 [\ n]”来做这个; +1并感谢你。 - David X
这是比调用 getchar 消耗尾随换行符更好的方法吗?这个格式规范需要特定的标准(例如:C99)吗?谢谢! - Cloud
3
@Dogbert: 老实说,我不太喜欢使用 scanf() 来解析输入。在大多数情况下,我更倾向于完全读取一行输入到缓冲区中,然后使用 sscanf()strtok() 或者其他更好的方式来解析该缓冲区。 - Michael Burr
@MichaelBurr 我也是这么想的。除了 * 以外,我几乎没有接触过这个 printf 格式说明符,所以看到是否有更好的习惯很好。谢谢! - Cloud
这个版本的问题在于,如果输入只是一个简单的“/n”(用户按下回车键而没有输入任何字符),它既无法匹配也无法消耗。 - lerosQ

7
当你使用scanf()读取字符串时,格式化字符串(%[^\n])告诉该函数读取每个不是'\n'的字符。这样就会使得'\n'字符留在输入缓冲区中。因此当你尝试读取str2str3时,scanf()每次都会发现缓冲区中的第一件事是'\n',并且由于格式化字符串的缘故,它不会从输入缓冲区中删除它。你需要做的是在每次从输入缓冲区中读取之间加入一个getchar()(通常紧跟在scanf()后面)。由于缓冲区中已经有了一个'\n',所以你的程序不会出现挂起的情况,因为它不必等待getchar()接收输入。试试看吧。 :)
对于那些不知道scanf()修饰符是什么的人,这里是来自http://linux.die.net/man/3/scanf的相关摘录:
引用: 匹配指定接受字符集合中的非空序列;其下一个指针必须是 char 类型的,指向存储匹配出来的字符序列的缓冲区。必须由调用者保证缓冲区足够大,足以存储读取到的字符串及其后面的空字符('\0'),并且下一个输入项在输入流中的开始处。 普通空白字符(空格、制表符、换行符等)会在需要时被跳过,除非格式字符串中的转换说明中包含 "%[]"。这个限定符用来从输入流中读取一连串不包含特定字符的字符串。当读取到格式字符串中指定的特定字符时,它会停止读取,并将结果返回给程序。

1
当您在格式字符串前添加一个前导空格时,它将被视为函数 isspace() 返回真值的任何字符,包括 '\n'' ''\t' 等。这意味着在格式字符串中下一个非空格字符之前的空格字符将被忽略。 - user539810
2
请注意,C99标准实际上并未指定"%["的行为。 - Chris Lutz
1
@Chris - 你说C99没有规定"%["是什么意思?它在17.9.6.2的"The fscanf function"中有说明。 - Michael Burr
@Michael Burr - 如果我没记错的话,我的副本(一个最终草稿之前的版本)将"%["的行为定义为实现定义。 - Chris Lutz
1
@Chris:有一些是实现定义的部分,比如使用“'-'”指定范围。 - Michael Burr
显示剩余4条评论

3

另外:

要读取一个字符串:

scanf("%[^\n]\n", a);

// 这意味着读取文本直到遇到 '\n',然后忽略该 '\n'

:)


6
最终的 '\n' 不仅会消耗一个 '\n',还会消耗任意数量的空白字符,例如 '\t'' ',以及 '\n'。它将继续消耗(丢弃)空白字符,直到找到非空白字符,然后将其放回到stdin中供下一次IO操作使用。 - chux - Reinstate Monica
什么是“trashing”?你的意思是它被消除了吗? - lmiguelvargasf

2
只需在scanf()函数后使用getchar()。

1
当时问题的主要点是理解行为。 - phoxis

1

我想要补充一下上面的回答-如果我们想要从输入流中删除一些特定的模式,比如0-9之间的数字,那么我们就需要使用getchar()来清除缓冲区。

scanf("%[^0-9\n]", str1);
while(getchar() != '\n'); // this approach is much better bcz it will
                         // remove any number of left characters in buffer.
scanf("%c", &ch);

在这里,如果你传递ashish019,那么只有ashish会被复制到str中,而019会留在缓冲区中,所以为了清除它,你需要多次调用getchar()。


-3

在读取每个输入后,使用fflush(stdin)清除输入缓冲区。


3
fflush() 只能用于输出流,如果在 stdin 上使用,虽然它似乎能正常工作,但标准定义了未定义的行为。因此,请勿在 stdin 上使用它。 - Maxim Chetrusca

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