缓冲流和非缓冲流

6

对于缓冲流,书中提到它会等待缓冲区填满后再回写到监视器。例如:

cout << "hi"; 
  1. What do they mean by "the buffer is full".

    cerr << "hi";
    
  2. It is said in my book that everything sent to cerr is written to the standard error device immediately, what does it mean?

    char *ch;
    cin>> ch; // I typed "hello world";
    
  3. In this example ch will be assigned to "hello" and "world" will be ignored does it mean that it still in the buffer and it will affect the results of future statements?


1
标准注释:默认情况下,coutstdio 同步,并且默认情况下 stdio 是行缓冲的。 - Yakov Galka
2
实际上,在第二个示例中,您将在未知位置写入内存并导致未定义的行为...希望最终导致崩溃。 - Edward Strange
cin >> ch 中,由于 ch 是一个指向字符字符串的指针且尚未初始化,因此它是一个“野指针”,这是未定义行为。其次,使用提取运算符 >>std::cin 一起读取时,当遇到第一个空格时会停止读取,而输入缓冲区中仍有剩余内容。 - Raindrop7
@YakovGalka,只是出于好奇,我们能否将这种行缓冲行为更改为其他内容? - Saurabh Rana
1
@SaurabhRana:是的。在stdout/stderr上调用setvbuf可以控制它们的缓冲(无/行/满)。根据我对标准的阅读,只要cout/cerrstdio同步(默认情况下),这应该有效地设置它们的缓冲区。 - Yakov Galka
5个回答

15

你的书似乎不是很有用。

1) 输出流将其字节发送到一个 std::streambuf,该缓冲区可能包含一个缓冲区;std::ofstream 所使用的 std::filebuf(派生自 streambuf)通常会被缓冲。这意味着当您输出字符时,它不一定会立即输出;它将被写入缓冲区,并且只有当缓冲区满或以某种方式明确请求时,一般通过在流上调用 flush()(直接或间接地,通过使用 std::endl)才会输出到操作系统。然而,这可能会有所不同;向 std::cout 输出会与 stdout 同步,并且大多数实现将更多或更少遵循 stdout 的规则来更改缓冲策略,如果输出将发送到交互式设备,则会更改缓冲策略。

无论如何,如果您不确定,并且想要确保输出真正离开您的程序,请添加一个调用 flush 的语句。

2) 这本书在这里是错误的。

其中之一缓冲策略是 unitbuf;这是 std::ostream 中的一个标志,您可以设置或重置它(std::ios_base::set()std::ios_base::unset()std::ios_basestd::ostream 的基类,因此您可以在 std::ostream 对象上调用这些函数)。当设置了 unitbuf 标志时,std::ostream 在每个输出函数的末尾添加一个调用 flush() 的语句,因此当您编写如下内容时:

std::cerr << "hello, world";
如果设置了unitbuf,则在字符串中的所有字符输出后,流将被清空。在启动时,std::cerr已经设置了unitbuf;默认情况下,在任何其他文件上都没有设置。但您可以根据需要自由设置或取消设置它。我建议不要取消std::cerr上的设置,但如果std::cout输出到交互设备,则在那里设置它是有意义的。
请注意,这里涉及的所有内容都是streambuf中的缓冲区。通常,操作系统也会进行缓冲。刷新缓冲区所做的全部工作就是将字符传输到操作系统;这个事实意味着在需要事务完整性时不能直接使用ofstream
当您使用>>从字符串或字符缓冲区输入时,std::istream首先跳过前导空格,然后输入直到遇到下一个空格为止。按照标准的正式术语,它会“提取”流中的字符,以便它们不会再次被看到(除非您寻找,如果流支持它)。下一个输入将从上一个输入离开的地方开始。以下字符是在缓冲区中还是仍然在磁盘上并不重要。
请注意,输入的缓冲有些复杂,因为它在几个不同的级别上发生,并且在OS级别上,它会根据设备的不同形式进行。通常,操作系统将通过扇区对文件进行缓冲,经常提前读取多个扇区。操作系统将始终返回与要求的字符数相同的字符,除非遇到文件结尾。大多数操作系统将通过行来缓冲键盘:在输入完整行之前不返回读取请求,并且在读取请求中不返回当前行的末尾之外的字符。
std::ostream使用streambuf输出一样,输入也使用streambuf进行缓冲。std::istream使用它来获取每个单独的字符。在std::cin的情况下,它通常是一个filebuf;当istream请求一个字符时,filebuf将从其缓冲区返回一个字符,如果有的话;如果没有,则会尝试重新填充缓冲区,例如从操作系统请求512(或其缓冲区大小)个字符。响应取决于设备的缓冲策略,如上文所述。
无论如何,如果std::cin连接到键盘,并且您已经输入了"hello world"所有的字符最终都会被流读取。(但如果使用 >>,会有很多您无法看到的空格。)

2
在C++中,流是缓冲区,用于提高效率。与内存操作相比,文件和控制台IO非常缓慢。为了解决这个问题,C++流有一个缓冲区(一组内存),其中包含要写入文件或输出的所有内容。当缓冲区满时,它会被刷新到文件中。对于输入来说,情况正好相反,当缓冲区被耗尽时,它会获取更多数据。这对于流非常重要,因为以下...
std::cout << 1 << "hello" << ' ' << "world\n";

将四次写入文件操作合并为一次,可以提高效率。

但是对于 std::cout、cin 和 cerr 这些类型,它们默认情况下已关闭缓冲以确保可以与 std::printf 和 std::puts 等函数一起使用。

要重新启用缓冲(我建议这样做):

std::ios_base::sync_with_stdio(false);

但是不要在控制台输出设置为false的情况下使用C风格的输出,否则可能会发生糟糕的事情。


所以在你的例子中,“1hello world\n”将首先传递到缓冲区,然后传递到屏幕上。但是如果我使用cerr<< 1 << "hello" << ' ' << "world\n";,那么1将被传递到屏幕上,然后是“hello”,依此类推...这样正确吗?谢谢。 - AlexDan
@AlexDan 嗯,“1hello world\n”将被写入缓冲区,当需要用户输入时,它将被写入屏幕,cin提取操作会同步这两个流,导致cout刷新。当您使用std::cout << std::flush或std::cout << std::endl手动刷新流时,或者当流被销毁和关闭(即对于std::cout的程序结束或对于文件的作用域结束)时,流也会被刷新。 - 111111
@AlexDan,Cerr和Cout的区别在于它们发送到的文件描述符不同,Cout发送到标准输出(stdout),而Cerr发送到标准错误(stderror)。当你将它们设置为不与stdio同步时,它们都会缓冲,当为true时,它们都是无缓冲的。话虽如此,clogcerr相同,但我认为它始终是缓冲的。 - 111111
@AlexDan 我刚刚用GCC尝试了一下,似乎在GCC中,即使使用std::ios_base::sync_with_stdio(false),std::cerr始终是无缓冲的。 - 111111

2
你可以使用一个小应用程序自己检查差异。
#include <iostream>
int main() {
    std::cout<<"Start";
    //std::cout<<"Start"<<std::endl; // line buffered, endl will flush.
    double j = 2;
    for(int i = 0; i < 100000000; i++){
        j = i / (i+1);
    }
    std::cout<<j;
    return 0;
}

尝试比较两个 "Start" 语句之间的区别,然后改用 cerr。你注意到的差异是由于缓冲造成的。

在我的计算机上,for循环需要大约2秒钟,您可能需要调整 i < 的条件以适应您的计算机。


抱歉,但我得到了相同的结果。开始后1秒钟后,我得到了0。 - AlexDan
如果您从此处所写的方式中获得了该结果,则表示您拥有一个未缓冲的 std::cout。 - Captain Giraffe
或者是非常慢的控制台。就像在Windows上 :) - sehe
+1 对于这个例子。正如人们所说的那样,比插图更好 :) - ajay

1
1)“缓冲区已满”是什么意思?
使用缓冲输出时,有一个称为缓冲区的内存区域,用于在实际写入输出之前存储要写出的内容。当您输入cout << "hi"时,该字符串可能只被复制到缓冲区中而未被写出。通常情况下,cout会等待该内存填满后才开始实际写出。
这样做的原因是因为通常启动实际写入数据的过程很慢,因此如果对每个字符都这样做,性能会非常差。使用缓冲区可以使程序只需偶尔执行此操作,从而获得更好的性能。
2)我的书上说,发送到cerr的所有内容都会立即写入标准错误设备,这是否意味着它会先发送'h'然后再发送'i'...?
这只是意味着不使用缓冲区。由于cerr已经拥有'h'和'i',因此它可能仍会同时发送它们。
3)在此示例中,ch将被分配为“hello”,而“world”将被忽略,这是否意味着它仍在缓冲区中,并且会影响未来语句的结果?
这实际上与缓冲区无关。char*的运算符 >> 的定义是读取直到遇到空格,因此它在“hello”和“world”之间的空格处停止。但是,是的,下次读取时你将获得“world”。
(虽然如果该代码不仅仅是实际代码的释义,则其具有未定义行为,因为您正在将文本读入未定义的内存位置。相反,您应该执行以下操作:
std::string s;
cin >> s;

)


0
  1. 每次写入终端的调用都很慢,因此为了避免频繁执行缓慢的操作,数据会存储在内存中,直到输入了一定量的数据或手动使用fflush或std::endl刷新缓冲区。这样做的结果有时是文本可能不会在您期望的时刻写入终端。

  2. 由于错误消息的时间更加关键,因此忽略性能损失并且不对数据进行缓冲。但是,由于字符串作为单个数据传递,因此它将在一个调用中写入(在某个循环内部)。

  3. 它仍然会在缓冲区中,但是通过在一个三行程序中尝试它,您可以很容易地证明这一点。但是,您的示例将失败,因为您正在尝试写入未分配的内存。您应该将输入放入std::string中。


第二点并不完全正确。std::cerrfilebuf仍然像往常一样进行缓冲。区别在于设置了unitbuf,这意味着在每个>>结束时ostream执行刷新。(实际上是在函数顶部构造sentry对象的析构函数中。) - James Kanze
@JamesKanze 是唯一能使缓冲区刷新的东西是 fflush 或 std::endl 吗?那 cout << "hello"; cout << "world"; 呢? - AlexDan
@AlexDan 不会。除非设置了 unitbuf,否则每个 '<<' 结尾处都会进行刷新。 - James Kanze

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