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

How is printf getting flushed before scanf is executed?

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

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

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

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

(我是运行ning ArchLinux)

编辑:由于一些评论说我的系统的标准输出是无缓冲的,我只想在我的程序中添加 运行ning scanf,我的程序具有我上面提到的行为,它肯定是缓冲的。

这是实施质量问题。

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

5.1.2.3 Program execution
[...]
The least requirements on a conforming implementation are:
[...]
The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.

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

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

中指定

Output streams that refer to terminal devices are always line buffered by default; pending output to such streams is written automatically whenever an input stream that refers to a terminal device is read. In cases where a large amount of computation is done after printing part of a line on an output terminal, it is necessary to fflush(3) the standard output before going off and computing so that the output will appear.

然而 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
    }

I thought that its buffer only gets flushed when a new line is read into the buffer or fflush(stdout) is called or the process that called printf exited normally.

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

中所述

… When a stream is line buffered, characters are intended to be transmitted to or from the host environment as a block when a new-line character is encountered. Furthermore, characters are intended to be transmitted as a block to the host environment when a buffer is filled, when input is requested on an unbuffered stream, or when input is requested on a line buffered stream that requires the transmission of characters from the host environment…

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

这是一个示例,说明如何从不相关的流中读取输入来刷新标准输出。当我使用 Xcode 11.3.1 在 macOS 10.14.6 上执行此程序时,当读取到 /dev/null 的无关流时,标准输出中的“Hello”被刷新,但当输出仅写入时不会刷新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.
}

如果我们删除 setvbuffgetc,“Hello”不会立即出现在终端上,表明它是对未缓冲流的读取导致标准输出脸红了。

I'm aware that printf is buffered…

这要视情况而定。其实就是流缓冲与否,C 2018 7.21.3 7 说:

… the standard input and standard output streams are fully buffered if and only if the stream can be determined not to refer to an interactive device.

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

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