在执行 scanf 之前 printf 如何被刷新?

问题描述

我最近阅读了很多关于标准输出缓冲的内容。我知道 printf 是缓冲的,但到目前为止我认为它的缓冲区只有在新行被读入缓冲区或 fflush(stdout) 被调用或调用 printf 的进程时才会被刷新正常退出。

我编写了这个程序,它在 scanf 之前没有换行就调用 printf。当我用谷歌搜索时,我发现很多人说他们不明白为什么在 printf 之前执行 scanf。由于我现在了解标准输出缓冲的概念,这对我来说很有意义。

然而,就我而言,缓冲区在我运行 scanf 之前被刷新。这样做确实有意义,因为用户可能希望在任何 scanf 之前执行 printf,但它是如何发生的?究竟什么是刷新标准输出?是scanf吗?

int main(void) {
    char things;
    printf("Hello ");
    scanf("%c",&things);
}

(我正在运行 Arch Linux)

编辑:由于一些评论说我的系统的标准输出是无缓冲的,我只想补充一点,而无需在我的程序上运行 scanf,我的程序完全具有我上面提到的行为,它绝对是缓冲的。

>

解决方法

这是一个实施质量问题。

C 标准仅要求 stdinstdout 在默认情况下仅在附加到常规文件时才被完全缓冲。但它明确鼓励交互式设备的特定行为:

5.1.2.3 程序执行
[...]
对一致实现的最低要求是:
[...]
交互设备的输入和输出动态应按照 7.21.3 的规定进行。这些要求的目的是尽快出现无缓冲或行缓冲的输出,以确保在程序等待输入之前实际出现提示消息。

在许多 Posix 系统上,stdinstdout 在连接到字符设备时是行缓冲的,然后当 stdout 的读取尝试需要读取时自动刷新 stdin来自底层系统句柄。这允许即使没有尾随换行符也能在终端上显示提示。

在 linux 上,此行为在 stdio(3) linux manual page 中指定:

引用终端设备的输出流总是行 默认缓冲;写入此类流的待处理输出 每当引用终端的输入流时自动 设备被读取。在大量计算的情况下 在输出终端上打印部分行后完成,它是 有必要在关闭之前 fflush(3) 标准输出和 计算以便出现输出。

然而 GNU libc 有一个微妙的不同行为:只有 stdout 以这种方式刷新,如 glibc/libio/fileops.c 中的编码(由 Ulrich Drepper 在 2001-08-04 修改23:59:30):

  /* Flush all line buffered files before reading. */
  /* FIXME This can/should be moved to genops ?? */
  if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
#if 0
      INTUSE(_IO_flush_all_linebuffered) ();
#else
      /* We used to flush all line-buffered stream.  This really isn't
         required by any standard.  My recollection is that
         traditional Unix systems did this for stdout.  stderr better
         not be line buffered.  So we do just that here
         explicitly.  --drepper */
      _IO_acquire_lock (_IO_stdout);

      if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
          == (_IO_LINKED | _IO_LINE_BUF))
        _IO_OVERFLOW (_IO_stdout,EOF);

      _IO_release_lock (_IO_stdout);
#endif
    }
,

我认为只有在将新行读入缓冲区或调用 fflush(stdout) 或调用 printf 的进程正常退出时,才会刷新其缓冲区。

这不是 C 标准的意图。当流被行缓冲时,只要程序请求任何未缓冲的流或从“主机环境”(例如用户键入输入的终端窗口)获取输入的行缓冲流上的输入,也应该刷新输出,如 C 2018 7.21.3 3 所述:

…当流被行缓冲时,当遇到换行符时,字符将作为块传输到主机环境或从主机环境传输。此外,当缓冲区被填满时,当在非缓冲流上请求输入时,或者当在需要传输字符的行缓冲流上请求输入时,字符将作为一个块传输到主机环境。宿主环境t ...

这只是表达了一个意图,标准进一步说对这些特性的支持是实现定义的,所以这在技术上是一个实现质量的问题。但是,就诊断消息的质量而言,这不是质量问题。关于支持是实现定义的以及关于标准输出是否行缓冲的保留在很大程度上是对各种旧计算机系统中的可行性或可能性的让步。在大多数现代 C 实现中,C 实现不应使用 C 标准的此许可作为不实现这些功能的借口。

以下是从无关流读取输入如何刷新标准输出的示例。当我使用 Xcode 11.3.1 在 macOS 10.14.6 上执行此程序时,读取与 /dev/null 无关的流时会刷新标准输出中的“Hello”,但仅在使用 {{1} 写入输出时不会刷新没有阅读:

printf

如果我们删除 #include <stdio.h> #include <unistd.h> int main(void) { printf("Hello"); FILE *dummy = fopen("/dev/null","r"); setvbuf(dummy,NULL,_IONBF,0); // Make dummy unbuffered. fgetc(dummy); // "Hello" appears on terminal. printf(" world."); // " world." does not appear on terminal. sleep(5); printf("\n"); // " world." appears on terminal. } setvbuf,“Hello”不会立即出现在终端上,这表明它是对无缓冲流的读取,导致标准输出被刷新.

我知道 fgetc 已缓冲...

这取决于具体情况。其实就是流有没有缓冲,C 2018 7.21.3 7 说:

…标准输入和标准输出流被完全缓冲当且仅当可以确定该流不引用交互式设备。

因此,如果你将程序的标准输出重定向到一个文件,它并不指向一个交互式设备,所以如果程序可以检测到它,它必须被完全缓冲。当程序的输出进入交互式终端窗口时,它不能被完全缓冲。 (替代方案是行缓冲和非缓冲,典型的 C 实现使用行缓冲。)'

因此,如果标准输入和标准输出都连接到交互式设备(并且无法检测到其他设备),那么 printf 输出应该在执行 printf 之前出现,因为标准输出是无缓冲的(所以 scanf 输出立即出现)或者因为标准输出是行缓冲的,并且在调用 printf 时刷新。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...