4.5 输入输出流缓冲

好的设计原则指出,无论何时创建一个新类,都应该尽最大努力隐藏实现细节。只让用户看到他们需要知道的东西,将其他东西都设为private以避免引起混乱。使用插入符和提取符时,一般程序员不知道或不必关心数据在哪里产生和消亡,不管处理的对象是标准I/O、文件、内存还是新创建的类或设备。

然而,重要的是与产生和消耗数据的输入输出流部分进行通信。为给这部分提供统一的接口并隐藏底层实现,标准库把它抽象成一个类,称为streambuf。每个输入输出流对象包含一个指向streambuf的指针。(对象类型依赖于被处理的内容是标准I/O、文件还是内存等。)用户可以直接访问streambuf;例如,可以把原始字节移入或移出streambuf,而不用通过封装它的输入输出流对其进行格式化。这可以通过调用streambuf对象的成员函数来完成。

目前,读者需要知道的最重要的事情,就是每个输出流对象包含一个指向streambuf对象的指针,并且streambuf对象拥有一些可供调用的成员函数。对于文件流类和字符串流类,他们有特殊的流缓冲区类型,如下图所示:

4.5 输入输出流缓冲 - 图1

为了能够访问streambuf,每个输入输出流对象有一个成员函数rdbuf(),它返回一个指向对象streambuf的指针,通过这个指针可以对streambuf对象进行存取。这样就可以对基础的streambuf调用任何成员函数。然而,更有趣的是,可以把streambuf指针和另一个输入输出流对象用运算符<<连接到一起。这将把右侧对象中的字符都输出到运算符<<左侧的对象中去。这样一来,如果想把一个输入输出流中的字符全部移动到另一个输入输出流中,就不需要通过令人厌烦的方法一次读一个字符或一行字符(会引起潜在的错误)。这是一种很优雅的方法。

下面是一个简单的示例程序,该程序打开一个文件并把文件中的内容送到到标准输出(和前面的例子相似):

4.5 输入输出流缓冲 - 图2

在这里使用这个程序的源代码文件作为参数创建了一个ifstream对象。如果文件不能打开,则函数assure()报告打开文件失败。所有的工作实际上都发生在如下语句中:

4.5 输入输出流缓冲 - 图3

这条语句把文件中的全部内容输出到cout。这样的语句不仅使得代码更简洁,而且常常比一次移动若干字节效率更高。

一种形式的get()函数直接把数据写入另一个对象的streambuf中。这个函数的第1个参数是目标streambuf的引用,第2个参数是使函数get()停止执行的结束字符(默认情况下为‘\n’)。如下所示,还有另外一种将文件发送到标准输出的方法:

4.5 输入输出流缓冲 - 图4

函数rdbuf()返回一个指针,它必须解除引用(dereference),以满足函数查看对象的需要。流缓冲区不会被复制(它们没有拷贝构造函数),所以将sb定义为cout的流缓冲区的引用。并调用函数fail_()和clear()以处理输入文件中的空行(在这个例子中就是如此)。当这个特殊的重载函数get()看到在一行中有两个新界定符(一个空行的证据),就设置输入流的失败位,所以必须调用clear()来重置失败位以继续从流中读取数据。对get()的第2次调用提取并回应每个新行的界定字符。(记住,get()和getline()提取界定字符的方法不同。)

也许并不需要经常使用这种技术,但是知道这种方法总是有益处的[1]

[1]关于流缓冲区和流的更深入的讨论可参考Addison-Wesley出版社1999年出版的Langer&Kreft的《Standard C++Iostreams and Locales》。