C语言 使用fflush(stdin)

3ks5zfa0  于 9个月前  发布在  其他
关注(0)|答案(7)|浏览(121)

所以在谷歌上搜索一下fflush(stdin)来清除输入缓冲区,会发现很多网站都警告不要使用它,而这正是我的计算机科学教授教我们的方法。
使用fflush(stdin)有多糟糕?我真的应该放弃使用它吗,即使我的教授正在使用它,而且它似乎毫无疑问地工作?

xvw2m8pv

xvw2m8pv1#

简单:这是未定义的行为,因为fflush是在输出流上调用的。这是C标准的摘录:
int findDuplicate();
ostream指向输出流或更新流,其中最近的操作没有被输入,fflush函数使该流的任何未写入数据被传递到主机环境以写入文件;否则,行为是未定义的。
因此,这不是一个“有多糟糕”的问题。fflush(stdin)只是不可移植,所以如果希望代码在编译器之间可移植,就不应该使用它。

flvlnr44

flvlnr442#

  • 将评论转换为答案 *

TL;DR -* 可移植代码不使用fflush(stdin) *

这个答案的其余部分解释了为什么可移植代码不使用fflush(stdin)。这很容易加上“可靠的代码不使用fflush(stdin)“,这通常也是正确的。

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

fflush()POSIX、C和C++标准明确声明行为是未定义的(因为stdin是输入流),但它们都不阻止系统定义它。
ISO/IEC 9899:2011 -C11标准-说明:

§7.21.5.2 fflush函数

如果stream指向一个输出流或一个更新流,其中最近的操作没有被输入,fflush函数会将该流的任何未写数据发送到主机环境,以写入文件;否则,行为是未定义的。
POSIX主要遵循C标准,但它确实将此文本标记为C扩展。
[CX]对于一个打开阅读的流,如果该文件还没有被打开,并且该文件是一个能够查找的文件,则底层打开文件描述的文件偏移量应被设置为该流的文件位置,并且由ungetc()ungetwc()推回到流上的、随后未从流中读取的任何字符都将被丢弃(不进一步更改文件偏移量)。
请注意,终端不能查找;管道和套接字也不能查找。

Microsoft将fflush(stdin)的行为定义为no-op

在2015年,Microsoft和Visual Studio运行时用于定义fflush()在输入流上的行为,如下所示(但链接导致2021年的文本不同-甚至“2015”版本中的文本也不同):
如果流是开放的,fflush将清除缓冲区的内容。
M.M注意事项:
Cygwin是一个相当常见的平台的例子,在这个平台上,fflush(stdin)不清除输入。
这就是为什么我的评论的这个回答版本注意到“微软和Visual Studio运行时”-如果你使用非Microsoft C运行时库,你看到的行为取决于该库。
Weather Vane在对另一个问题的评论中向我指出,在2021年6月之前的某个时候,微软改变了对fflush()的描述,与2015年编写这个答案时最初指定的内容相比,它现在说:
如果流是在读模式下打开的,或者流没有缓冲区,则对fflush的调用无效,所有缓冲区都将保留。对fflush的调用将取消任何先前对ungetc的调用对流的影响。
Caveat Lector:最好不要在任何平台上依赖fflush(stdin)

Linux的文档和实践似乎是矛盾的

令人惊讶的是,Linux名义上也记录了fflush(stdin)的行为,甚至以同样的方式定义了它(奇迹中的奇迹)。
对于输入流,fflush()将丢弃从基础文件中获取但尚未被应用程序使用的任何缓冲数据。
在2021年,报价更改为:
对于输入流,fflush()会丢弃所有从底层文件中提取但尚未被应用程序使用的缓冲数据。流的打开状态不受影响。
Linux上的fflush(3)的另一个来源也同意(给予或接受段落休息):
对于与可搜索文件(例如磁盘文件,但不是管道或终端)相关联的输入流,fflush()将丢弃从底层文件中获取但尚未被应用程序使用的任何缓冲数据。
这些都没有明确地解决POSIX规范关于ungetc()的问题。
2021年zwol评论说Linux文档已经改进了,在我看来,仍然有改进的空间。
在2015年,我对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;
}

字符串

输出示例

$ ./demo-fflush
Alliteration
Got A; enter some new data
Got l
$


这个输出是在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;
}

输出示例

请注意,/etc/passwd是一个可查找的文件。在Ubuntu上,第一行看起来像:

root:x:0:0:root:/root:/bin/bash


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

##
# User Database
# 
# Note that this file is consulted directly only when the system is running


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

root:*:0:0:System Administrator:/var/root:/bin/sh


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
$


Mac OS X 10.11.2:

$ ./demo-fflush2 < /etc/passwd
Got #
Got Z after ungetc()
Got B
$

Mac OS X的行为忽略了(或者至少看起来忽略了)fflush(stdin)(因此在这个问题上不遵循POSIX)。Linux的行为对应于文档中的POSIX行为,但是POSIX规范在它所说的内容上要谨慎得多-它指定了一个能够查找的文件,但是终端,当然,不支持查找。它也比微软规范少得多有用。

摘要

微软记录了fflush(stdin)的行为,但这种行为在2015年至2021年之间发生了变化。显然,它在Windows平台上使用本机Windows编译器和C运行时支持库进行工作。
尽管有相反的文档,但当标准输入是终端时,它不能在Linux上工作,但它似乎遵循POSIX规范,该规范措辞更为谨慎。根据C标准,fflush(stdin)的行为是未定义的。POSIX添加了限定符“除非输入文件是可搜索的”,而终端不是。行为与Microsoft的不一样。
因此,* 可移植代码不使用fflush(stdin) *。绑定到Microsoft平台的代码可以使用它,并且它可以按预期工作,但要注意可移植性问题。

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

从终端文件描述符(与stdin这样的文件流相反)中丢弃未读信息的POSIX标准方法在How can I flush unread data from a tty input queue on a Unix system中说明。然而,这是在标准I/O库级别下操作的。

ax6ht2ek

ax6ht2ek3#

根据标准,fflush只能用于输出缓冲区,显然stdin不是一个。然而,some标准C库提供了fflush(stdin)作为扩展的使用。在这种情况下,你可以使用它,但它会影响可移植性,所以你将不再能够使用地球上任何符合标准的标准C库,并期望同样的结果。

oxf4rvwz

oxf4rvwz4#

我相信你永远不应该调用fflush(stdin),原因很简单,你甚至不应该发现有必要在第一时间尝试刷新输入。现实地说,你可能认为你必须刷新输入的原因只有一个,那就是:跳过scanf卡住的一些坏输入。
例如,您可能有一个程序正在循环阅读使用scanf("%d", &n)的整数,很快您就会发现,当用户第一次键入非数字字符(如'x')时,the program goes into an infinite loop
面对这种情况,我相信你基本上有三种选择:
1.以某种方式刷新输入(如果不使用fflush(stdin),则在循环中调用getchar以读取\n之前的字符,这是通常推荐的)。
1.告知用户不要在需要数字时键入非数字字符。

  1. Use something other than scanf to read input的一个。
    现在,如果你是一个初学者,scanf似乎是最简单的读取输入的方法,所以选择#3看起来很可怕也很困难。但是#2似乎是一个真实的的逃避,因为每个人都知道不友好的计算机程序是一个问题,所以做得更好会更好。所以太多的初学者被逼到了角落。感觉他们别无选择,只能做#1。他们或多或少必须使用scanf进行输入,这意味着它会在错误的输入上卡住,这意味着他们必须找到一种方法来刷新错误的输入,这意味着他们非常想使用fflush(stdin)
    我想鼓励所有初学C语言的程序员做出不同的权衡:
    1.在你C语言编程生涯的早期阶段,在你习惯使用scanf以外的任何东西之前,只要 * 不要担心错误的输入 *。真的。继续使用上面的第二个借口吧。这样想:你是一个初学者,有很多事情你还不知道如何去做,其中一件你还不知道如何去做的事情是:优雅地处理意外输入。
  2. learn how to do input using functions other than scanf。到那时,您就可以开始优雅地处理错误的输入了,而且您可以使用更多更好的技术,根本不需要尝试“清 debugging 误的输入”。
    或者,换句话说,那些还在使用scanf的初学者应该放心地使用cop-out #2,当他们准备好的时候,他们应该从那里升级到技术#3,没有人应该使用技术#1来尝试刷新输入--当然不是使用fflush(stdin)
jv4diomz

jv4diomz5#

使用fflush(stdin)刷新输入有点像dowsing for water使用一个形状像字母“S”的棍子。
帮助人们以某种“更好”的方式刷新输入就像冲到一个S形的探测器前说:“不,不,你做错了,你需要使用Y形的探测器!”
换句话说,真实的问题不是fflush(stdin)不工作。调用fflush(stdin)是一个潜在问题的症状。为什么你必须“刷新”输入?这是你的问题。
而且,通常情况下,潜在的问题是,您正在使用scanf,在其许多无用的模式之一,意外地在输入中留下换行符或其他“不需要的”文本。因此,最好的长期解决方案是learn how to do input using better techniques than scanf,这样您就不必处理其未经处理的输入和其他特性。

r7xajy2e

r7xajy2e6#

现有的答案都没有指出这个问题的一个关键方面。
如果你发现自己想要“清除输入缓冲区”,你可能正在编写一个命令行交互式程序,更准确地说,你想要的是“丢弃当前输入行中你还没有读过的字符”。

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

那么你应该怎么做呢?我更喜欢的方法是,如果你一次处理一行输入,那么 * 一次读取一整行 *。C库有专门的函数:fgets个(在C90中,完全可移植,但仍然使您可以分块处理很长的行)和getline(特定于POSIX,但会为您管理一个malloc艾德缓冲区,这样您就可以一次处理所有的长行,不管它们有多长)。通常会有一个直接的翻译,从处理“当前行”的代码直接从stdin翻译为处理包含“当前行”的字符串的代码。

bqjvbblv

bqjvbblv7#

引用自POSIX
对于打开用于阅读的流,如果文件还没有处于打开状态,并且文件是能够查找的文件,则底层打开文件描述的文件偏移应被设置为流的文件位置,并且由ungec()或ungetwc()推回到流上的任何随后未从流读取的字符应被丢弃(不进一步改变文件偏移)。
请注意,终端无法查找。

相关问题