C++缓冲流IO

5

我知道C++默认支持的所有流IO都是带缓冲的。

这意味着要输出的数据被放入缓冲区直到满了,然后发送到输出设备,同样地,对于输入,数据在缓冲区为空时读取...所有这些都是为了最小化昂贵的系统调用次数。

但是如何验证这种行为呢?我的意思是考虑以下代码:

int main()
{
    cout << "Hello world\n";
    return 0
}

这里缓冲是什么意思?我知道有缓冲发生,但怎样解释它呢?输出立即出现在屏幕上,那么有哪些代码示例可以真正看到缓冲输入/输出的实际情况呢?


在cout的情况下,缓冲区为1字节。真正的缓冲是在读/写文件时发挥作用。 - Mr.Anubis
@Mr.Anubis 你是怎么得出这个结论的?不,这并不一定是真的。 - Konrad Rudolph
@KonradRudolph 嗯,我不记得那句话的来源,但在大多数情况下确实是这样。 - Mr.Anubis
@Mr.Anubis 标准流中没有使用1字节缓冲区的情况。std::cout通常是有缓冲的。(这与C中的stdout不同,如果它连接到交互设备,则为行缓冲,否则为完全缓冲。C++没有行缓冲的概念;使用std::endl可以有效地模拟它。) - James Kanze
@JamesKanze 默认情况下,C++流不被缓冲实际上是因为C++流直接写入C stdio流,这就是它们可以被同步的原因。只有在显式地取消同步时才会进行缓冲。 - user11877195
3个回答

8
首先,并不是所有的iostream都是缓冲的;缓冲是由附加的streambuf处理的。对于filebuf(被ifstream和ofstream使用),输入将尽可能地读取,直到缓冲区的大小为止,输出将在溢出时刷新缓冲区,在显式刷新或关闭发生时刷新缓冲区,或在对象被销毁时刷新缓冲区(这会隐式调用close)。
cout的情况有点特殊,因为它从不被销毁也不关闭。系统保证flush将在调用exit后至少调用一次(在从main返回时会发生这种情况)。这意味着在从main返回之前的任何输出都将被刷新;如果你在静态对象的析构函数中使用cout,仍然需要显式刷新才能确保。
还可以将一个输出流与一个输入流绑定;cout默认绑定到cin。在这种情况下,任何尝试从绑定的流中输入的操作都会刷新输出。
通常的约定是只使用std::endl而不是简单地输出'\n';std::endl输出一个'\n',然后刷新流。对于非常重要的所有输出都及时出现的流,可以设置unitbuf标志,这意味着流将在每个<<运算符结束时被刷新。(std::cerr默认设置了这个标志。)
最后,如果你想看到缓冲的效果,请在输出后加上类似sleep(10)的东西。如果它立即显示了输出,那么它已经被刷新;如果没有,那么它已经被缓冲,并且在sleep之后隐式地进行了刷新。

谢谢您的回复。但如果您能提供一个一直有效的代码示例,那就太好了。 - Arun
@Arun 一直在做什么?使用std::endl代替'\n'会导致每行末尾都刷新。如果你需要更多,std::cout << std::flush将无条件地刷新输出。 - James Kanze
我需要一个例子来说明缓冲区的作用。类似于:cout << "Hello world"; // 这里控制台上没有输出 cout << endl; // 现在控制台上可以看到 Hello world。 - Arun
@Arun 所以在这两个语句之间加入延迟,你应该会发现在延迟之前没有任何输出。 - James Kanze
尝试在VS2008和VS2010上运行,但没有成功。语句立即输出到屏幕上,因此没有观察到缓冲的证据。 - Arun
@Arunm 所以可能 VC2010 使用非常小的缓冲区。或者如果输出设备是交互式的,则更改其缓冲策略。(或者,我认为,编译器由于实现与stdio同步的方式而最终执行单元缓冲。) - James Kanze

5
请尝试以下程序。使用sleep(1)来引入1秒的延迟,我使用的是Linux系统,所以sleep对我有效。如果您无法使其正常运行,请尝试其他方法来延迟此程序(例如简单的for循环)。如果您没有看到任何缓冲效果,您还可以尝试增加缓冲区大小(取消已注释的代码行)。
在我的操作系统(Linux 3.2.0)和编译器(g++ 4.6.3)上,此程序会先打印“Portion1Portion2”,然后是“Portion3Portion4”,最后是“Portion5”。std::endl保证刷新缓冲区,但是如您所见,换行符也可以以这种方式工作。
#include <iostream>
#include <unistd.h>

using namespace std;

int main () {
    // Try uncommenting following lines to increase buffer size
    // char mybuf[1024];
    // cout.rdbuf()->pubsetbuf(mybuf, 1024);

    cout << "Portion1";
    sleep(1);
    cout << "Portion2\n";
    sleep(1);
    cout << "Portion3";
    sleep(1);
    cout << "Portion4" << endl;
    sleep(1);
    cout << "Portion5" << endl;
    sleep(1);
    cout << "Done!" << endl;

    return 0;
}

1
即使取消注释,仍然如此吗? - Alexander Putilin

4

尝试以下代码:

int main()
{
    for( int i =0 ; i < 10; i ++ )
    {
        cout << i << " ";
        cerr << i << " ";
    }
}

缓冲输出通常在流对象销毁时刷新,因此上面的代码将打印输出(当然,并非总是如此,但使用gcc 4.6.3对我来说确实如此)。

0 1 2 3..9
0 1 2 3..9

替换为
0 0 1 1 2 2 3 3 .... 9 9 

我的输出

由于非缓冲的 cerr 会立即打印出来(第一个序列),而缓冲的 cout 会在 main() 结束时打印。


@Arun,这就是我说“不总是”的原因。缓冲区是实现特定的,因此在不同的编译器和设置下会看到不同的结果。有时输出甚至会以奇怪的顺序混合。 - SingerOfTheFall
std::cout 永远不会被解构,但缓冲区可能因其他原因而被刷新。在这种情况下,std::cinstd::cerr 都被绑定到 std::cout,这意味着任何一个它们的 IO 操作都会刷新 std::cout。并且 std::cerr 是单元缓冲的,这意味着每个 << 运算符在操作结束时都会将其刷新。 - James Kanze
@SingerOfTheFall,难道就没有一种始终有效的一致性示例吗?我尝试了问题中的所有示例,但都不起作用...这是否意味着在VS2010中,它总是会刷新缓冲区? - Arun
@Arun,抱歉,我不知道有一个一致的例子。我已经发布了我的输出图像,请检查答案。 - SingerOfTheFall
@SingerOfTheFall 这可能(很可能)取决于库的版本。C++11要求将std::cout绑定到std::cerr(除了std::cin);这意味着任何输出到std::cerr都会刷新std::cout,从而得到“0 0 1 1 ...”。C++03不允许它被绑定,因此通常会给出“0 1 2... 0 1 2...”。 - James Kanze
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接