使用fflush(stdin)

wro*_*ame 69 c stdin fflush

因此,谷歌快速搜索fflush(stdin)清除输入缓冲区会发现许多网站警告不要使用它.然而,这正是我的CS教授教授课程的原因.

使用有多糟糕fflush(stdin)?即使我的教授正在使用它并且似乎完美无缺地工作,我是否真的应该放弃使用它?

Eli*_*sky 67

简单:这是未定义的行为,因为fflush它意味着在输出流上调用.这是C标准的摘录:

int fflush(FILE*ostream);

ostream指向输入流或未输入最近操作的更新流,fflush函数导致该流的任何未写入数据被传递到主机环境以写入文件; 否则,行为未定义.

所以这不是一个"多么糟糕"的问题.显然fflush(stdin)错的,你永远不能使用它.

  • @BlueRaja:这里有一个新手错误的辩护,但**没有防御老师**传播错误的知识!对`fflush`的任何引用都明确表示它适用于第一段中的输出流,您不必为此记住C标准! (16认同)
  • @Eli:没人知道一切.处理器永远不会知道他的错误,直到有人告诉他...我用了`fflush(stdin)`多年,直到我发现它是UB(偶然) (6认同)
  • 另一个防范点是手册页的以下部分(Linux上的各种glibc版本):"对于输入流,`fflush()`丢弃从底层文件中提取但尚未被消耗的任何缓冲数据应用程序.流的开放状态不受影响." 虽然它是UB,但是一些实现似乎在不提及其相对于标准的状态的情况下提供保证. (5认同)
  • 呃,在使用它之前,通常不应该查看函数的文档吗?特别是教授? (4认同)
  • 我猜大学教授也是人类:-) (3认同)
  • @VictorZamanian:这取决于您所说的“未定义行为”是什么意思。在 Linux 上,手册定义了行为;使用 `fflush(stdin)` 是 Linux 上定义的行为。它不是根据标准定义的(因此在这种情况下它不符合任何标准规定的行为),而是由 Linux(或更准确地说,由 glibc)定义的。 (3认同)
  • 还有一个我很少看到提到的方面:`fflush(stdin)`比实现定义的行为更糟​​糕.**即使它确实像大多数人想要的那样工作,也会很糟糕.**想象一下,如果stdin不是某人愚蠢地输入输入,而是来自另一个程序或shell重定向:它会读取文件的开头然后只是擦除其余的部分.认为stdin总是像人类操作员一样慢,这真是愚蠢. (3认同)
  • @DanielFischer:在glibc手册页的"CONFORMING TO"部分下:"标准没有指定输入流的行为." 这有时是一个很好的部分.:)所以即使glibc提到它不是标准行为,甚至它是未定义的.因此,它不应该用于标准兼容/便携式/有什么用途. (2认同)

Jon*_*ler 36

将注释转换为答案 - 并在问题再次出现时扩展它们.

标准C和POSIX保留fflush(stdin)未定义的行为

POSIX,C和C++标准fflush()明确规定,该行为是不确定的,但它们都没有防止系统定义它.

ISO/IEC 9899:2011 - C11标准 - 说:

§7.21.5.2fflush功能

2如果stream指向输入流或未输入最新操作的更新流,则该fflush函数会将该流的任何未写入数据传送到主机环境以写入该文件; 否则,行为未定义.

POSIX主要遵循C标准,但它确实将此文本标记为C扩展名.

[CX]对于打开读取的流,如果文件尚未处于EOF,并且文件是能够搜索的文件,则底层打开文件描述的文件偏移量应设置为流的文件位置,并且将被丢弃的字符ungetc()或者ungetwc()之后未从流中读取的字符将被丢弃(不再进一步改变文件偏移量).

注意终端不能寻求; 管道或插座都不是.

微软定义了它的行为 fflush(stdin)

Microsoft和Visual Studio运行时定义了fflush()在输入流上定义行为.

如果流已打开以进行输入,则fflush清除缓冲区的内容.

MM 注意到:

Cygwin是一个相当常见的平台的例子,它fflush(stdin)没有清除输入.

这就是为什么我的评论的答案版本注意到"Microsoft和Visual Studio运行时" - 如果您使用非Microsoft C运行时库,您看到的行为取决于该库.

Linux文档和实践似乎相互矛盾

令人惊讶的是,Linux名义上也记录了它的行为fflush(stdin),甚至以同样的方式定义它(奇迹的奇迹).

对于输入流,fflush()丢弃从底层文件中提取但尚未被应用程序使用的任何缓冲数据.

我仍然有点困惑,并对Linux文档说它fflush(stdin)会起作用感到惊讶.尽管有这样的建议,但它通常不适用于Linux.我刚检查了Ubuntu 14.04 LTS的文档; 它说上面引用的内容,但根据经验,它不起作用 - 至少当输入流是一个不可搜索的设备,如终端时.

demo-fflush.c

#include <stdio.h>

int main(void)
{
    int c;
    if ((c = getchar()) != EOF)
    {
        printf("Got %c; enter some new data\n", c);
        fflush(stdin);
    }
    if ((c = getchar()) != EOF)
        printf("Got %c\n", c);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

示例输出

$ ./demo-fflush
Alliteration
Got A; enter some new data
Got l
$
Run Code Online (Sandbox Code Playgroud)

此输出是在Ubuntu 14.04 LTS和Mac OS X 10.11.2上获得的.据我所知,它与Linux手册所说的相矛盾.如果fflush(stdin)操作有效,我将不得不输入一行新文本来获取第二行的信息getchar().

鉴于POSIX标准所说的,可能需要更好的演示,并且应该澄清Linux文档.

demo-fflush2.c

#include <stdio.h>

int main(void)
{
    int c;
    if ((c = getchar()) != EOF)
    {
        printf("Got %c\n", c);
        ungetc('B', stdin);
        ungetc('Z', stdin);
        if ((c = getchar()) == EOF)
        {
            fprintf(stderr, "Huh?!\n");
            return 1;
        }
        printf("Got %c after ungetc()\n", c);
        fflush(stdin);
    }
    if ((c = getchar()) != EOF)
        printf("Got %c\n", c);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

示例输出

请注意,这/etc/passwd是一个可搜索的文件.在Ubuntu上,第一行看起来像:

root:x:0:0:root:/root:/bin/bash
Run Code Online (Sandbox Code Playgroud)

在Mac OS X上,前4行看起来像:

##
# User Database
# 
# Note that this file is consulted directly only when the system is running
Run Code Online (Sandbox Code Playgroud)

换句话说,Mac OS X /etc/passwd文件的顶部有评论.非注释行符合正常布局,因此root条目为:

root:*:0:0:System Administrator:/var/root:/bin/sh
Run Code Online (Sandbox Code Playgroud)

Ubuntu 14.04 LTS:

$ ./demo-fflush2 < /etc/passwd
Got r
Got Z after ungetc()
Got o
$ ./demo-fflush2
Allotrope
Got A
Got Z after ungetc()
Got B
$
Run Code Online (Sandbox Code Playgroud)

Mac OS X 10.11.2:

$ ./demo-fflush2 < /etc/passwd
Got #
Got Z after ungetc()
Got B
$
Run Code Online (Sandbox Code Playgroud)

Mac OS X行为忽略(或至少似乎忽略)fflush(stdin)(因此在此问题上不遵循POSIX).Linux行为对应于记录的POSIX行为,但POSIX规范在其所说的内容中要小心得多 - 它指定了一个能够搜索的文件,但终端当然不支持搜索.它也没有微软规范那么有用.

摘要

Microsoft记录了该行为fflush(stdin).显然,它使用本机Windows编译器和C运行时支持库,在Windows平台上记录.

尽管有相反的文档,但当标准输入是一个终端时,它在Linux上不起作用,但它似乎遵循POSIX规范,这更加谨慎.根据C标准,行为fflush(stdin)是不确定的.POSIX添加限定符'除非输入文件是可搜索的',终端不是.这种行为与微软的行为不同.

因此,便携式代码不使用fflush(stdin).与Microsoft平台相关的代码可能会使用它并且它可以工作,但要注意可移植性问题.

POSIX方法从文件描述符中丢弃未读终端输入

从终端文件描述符中丢弃未读信息的POSIX标准方法(与文件流相反stdin)如下所示:如何从Unix系统上的tty输入队列中清除未读数据.但是,它在标准I/O库级别以下运行.

  • 在OP要求的背景下,这可能是更好的答案,尽管接受的答案并没有错。一方面清楚地表明它不符合标准,但另一方面表明它可以正确地用于特定的实现。+1 (2认同)
  • @zwol — 我已经用 Linux 手册中的修订引用再次更新了答案(如 man7.org 和 die.net 上找到的)。这些手册页更好,但仍然没有指定 POSIX 标准关于丢弃由 `ungetc()` 推回的字符的规定。在我看来,他们仍然过于关注绝对最小化,从而损害了意图的清晰度。 (2认同)

tif*_*tik 18

根据标准,fflush只能用于输出缓冲区,显然stdin不是一个.但是,一些编译器提供了fflush(stdin)作为扩展的用法.在这种情况下,您可以使用它,但它会影响可移植性,因此您将无法再在地球上使用任何符合标准的编译器并期望获得相同的结果.


Ste*_*mit 12

我相信你永远不应该调用fflush(stdin),原因很简单,你甚至不应该首先发现有必要尝试刷新输入。实际上,您可能认为必须刷新输入的原因只有一个,那就是:绕过一些scanf卡住的错误输入。

例如,您可能有一个使用scanf("%d", &n). 很快您就会发现,当用户第一次键入非数字字符时'x'程序会进入无限循环

面对这种情况,相信你基本上有三个选择:

  1. 以某种方式刷新输入(如果不是使用fflush(stdin),则通过调用getchar循环来读取字符直到\n,这通常是推荐的)。
  2. 告诉用户在需要数字时不要输入非数字字符。
  3. 使用除scanf读取 input以外的其他内容

现在,如果您是初学者,这scanf 似乎是读取输入的最简单方法,因此选项 #3 看起来既可怕又困难。但是#2 似乎是一个真正的逃避,因为每个人都知道对用户不友好的计算机程序是一个问题,所以最好做得更好。因此,太多的新手程序员陷入了困境,觉得他们别无选择,只能做 #1。他们或多或少必须使用 进行输入scanf,这意味着它会卡在错误输入上,这意味着他们必须想办法清除错误输入,这意味着他们非常想使用fflush(stdin).

我想鼓励所有初学 C 程序员做出不同的权衡:

  1. 在 C 编程生涯的早期阶段,在您习惯使用 之外的任何东西之前scanf不要担心错误的输入。真的。继续使用上面的 cop-out #2。可以这样想:你是一个初学者,有很多事情你还不知道怎么做,而你还不知道怎么做的事情之一是:优雅地处理意外的输入。

  2. 尽快学习如何使用scanf. 在这一点上,您可以开始优雅地处理错误输入,并且您将拥有更多、更好的技术可用,而根本不需要尝试“刷新错误输入”。

或者,换句话说,仍然坚持使用的初学者scanf应该随意使用 cop-out #2,当他们准备好时,他们应该从那里毕业到技术 #3,没有人应该使用技术 #1 来尝试完全刷新输入 - 当然不是fflush(stdin).


Ste*_*mit 5

使用fflush(stdin)冲洗输入有点像使用形状像字母“S”的棒来寻找水

帮助人们以某种​​“更好”的方式刷新输入有点像冲到 S-stick dowser 说“不,不,你做错了,你需要使用 Y 形杆!”。

换句话说,真正的问题fflush(stdin)不是不起作用。呼叫fflush(stdin)是潜在问题的征兆。为什么你必须“刷新”输入? 那是你的问题。

而且,通常,潜在的问题是您正在使用scanf, 在其许多无用的模式之一中意外地在输入中留下换行符或其他“不需要的”文本。因此,最好的长期解决方案是学习如何使用比 更好的技术进行输入scanf,这样您就完全不必处理未处理的输入和其他特性。


zwo*_*wol 5

现有的答案都没有指出问题的关键方面。

如果您发现自己想要“清除输入缓冲区”,那么您可能正在编写一个命令行交互式程序,更准确地说,您想要的是丢弃当前输入中尚未删除的字符还没有读过。

这不是fflush(stdin)做的事。fflush支持在输入流上 使用的 C 库将其记录为不执行任何操作,或者丢弃已从底层文件读取但未传递到应用程序的缓冲数据。这很容易比当前行的其余部分输入更多更少。在很多情况下,它可能会意外地工作,因为终端驱动程序(在默认模式下)一次一行地向命令行交互程序提供输入。然而,当您尝试从磁盘上的实际文件向程序提供输入时(可能是为了自动化测试),内核和 C 库将切换到以大“块”(通常为 4 到 8 kB)缓冲数据,而不会产生任何影响。与行边界的关系,您会想知道为什么您的程序正在处理文件的第一行,然后跳过几十行并在下面一些明显随机的行的中间进行拾取。或者,如果您决定在手写的很长的一行上测试您的程序,那么终端驱动程序将无法立即为程序提供整行,也fflush(stdin)不会跳过所有行。

那么你应该做什么呢?我更喜欢的方法是,如果您一次处理一行输入,则一次读取一整行。C 库有专门用于此目的的函数:(fgets在 C90 中,因此完全可移植,但仍然使您可以分块处理很长的行)和getline(POSIX 特定的,但将为malloc您管理一个 ed 缓冲区,以便您可以处理所有长行立即,无论他们需要多长时间)。通常存在从直接从 stdin 处理“当前行”的代码到处理包含“当前行”的字符串的代码的直接转换。

  • @AbhishekMane C 实现可以定义标准未定义的行为,并且 C 程序可以自由依赖此类定义;它只是让它们不太便携。这个答案的要点是,_即使你可以依赖实现扩展_,使用 `fflush(stdin)` 仍然是一个错误,因为 `fflush(stdin)` 的常见实现扩展并没有做你真正想要的事情。 (2认同)