输出流的缓冲区

12/12/2015

Intro

这段时间,不断听到一年级的萌新们在问,fflush是什么函数。为什么有了它就可以正常地输入输出,反而就会少些什么。

首先需要说明的是,在 C 语言中,不应该依赖 fflush 函数解决 I/O 问题。因为它只在某些编译器上有效。很多比较主流的编译器,比如 gcc,就并不支持这个这个函数。它是对C语言标准的扩充,虽然在 C99 中有定义,但不对所有编译器有效。

我们从文档中查到的这个函数的声明方法是 int fflush(FILE *stream); ——这里我们可以认为这个 int 类型的返回值其实是担当了 bool 类型的作用,用来判断这个操作是否成功。

具体的执行情况是:如果参数是输出流(output stream),fflush函数会把还没有输出的数据都传给这个流指向的文件。举例: fflush(stdout); 。否则,fflush 函数的行为即为未定义行为,如果发生了写入相关的错误,返回值是EOF(-1)。

下面,我们从 C++ 的角度研究一些和缓冲区有关的问题。

Output Buffer

尝试用代码向输出流添加一条输出指令,如: os << “Set output to output buffer.”; 。执行的效果可能是立刻就被打印了出来,也有可能没什么反应。这里我们只是给输出流发送指令,而并不是常规意义上的 cout,使得情况可能有所不同。假想以下情况:操作系统需要同时处理一系列输出流的输出到设备操作,而设备的写操作可能非常耗时(比如打印机等输出时需要耗时的操作),操作系统每接到一次输出流的指令就做一次操作显然浪费了系统资源和操作时间。于是,操作系统把这些指令暂时放在输出缓冲区(output buffer)里,接收到缓冲刷新的指令后,统一将多个类似的操作指令组合成单一的系统级写操作。这样,性能得到了极大的提升。

刷新缓冲的原因可能有:

  1. main 函数的 return 操作让程序正常地结束
  2. 缓冲区满,如果不刷新缓存则其他的指令不能进入缓冲区
  3. 利用操纵符显式刷新缓冲区(比如熟悉的 std::endl)
  4. 利用操纵符设置流的状态并清空缓冲区(比如 unitbuf,如果对这个东西不熟悉,那么更熟悉一些的 cerr 的内容都是默认设置  unitbuf  的)
  5. 一个输出流被关联到另一个流(比如 cin 和 cerr 都关联到 cout,因此这两种操作都会使得 cout 的缓冲区刷新)

下面介绍几个实例:

cout << "Output with a newline, refresh buffer." << endl;
cout << "Output, refresh buffer." << flush;
cout << "Output with a blank, refresh buffer." << ends;

当然,每次输出都和缓冲区打交道是很累的。这时,我们可以借用 unitbuf 指令,告诉系统之后的所有的输出都自带 flush 操作,直到我们给某次输出 nounitbuf,比如:

cout << unitbuf; //After this, every output will refresh buffer.
cout << nounitbuf; //Back to usual status.

Worthy to Say

很多情况下,萌新们辛辛苦苦写了很多代码却见不到输出。一方面可能是确实没有输出内容,还有可能是程序崩溃导致这些输出被滞留在缓冲区里面等待打印了。所以,调试的时候必须保证输出缓冲区的数据确实被刷新了,否则就会一直纠结于为什么没有输出结果这个问题上(我个人建议先写出一份可行的版本,也就是编译和运行都没有问题的,之后添加复杂的功能以满足需求)。

One More Thing

在交互较多的情况下,cin 之前必须有 cout 信息输出到设备,这时我们可以把输入流关联到输出流,让每次输入之前都刷新输出缓冲,示例:

cin >> ival;