理解c++中的缓冲区

327 阅读4分钟

为什么要有缓冲区

众所周知,相对于 CPU 的指令执行和主存访问,I/O 操作是非常慢的。这也就是说,在不考虑缓冲区的情况下,如果程序有频繁的 I/O 操作,那么相当于程序的「高速」部分就会被频繁打断。这对于程序的整体性能是不利的。有了缓冲区,程序就可以避免频繁的 I/O 操作,而是对缓冲区进行读写,只有在必须的情况下,才通过刷新缓冲区进行真实的 I/O 操作。这样一来,程序就能将多个缓慢的 I/O 操作合并成一个,从而在整体上提高了程序的性能。

缓冲区需要做那些工作

从上一节的描述中,不难发现缓冲区向上连接了程序的输入输出请求,向下连接了真实的 I/O 操作。作为中间层,必然需要分别处理好与上下两层之间的接口,以及要处理好上下两层之间的协作。(后者即是中间层本身的功能)

在 C++ 中,流的缓冲区之基类是定义在 streambuf 头文件当中的 std::basic_streambuf。这是一个类模板;其声明如下:

template<
    class CharT,
    class Traits = std::char_traits<CharT>
> class basic_streambuf;

std::basic_streambuf 包含两个字符序列,并提供对这两个序列控制和访问的能力:

  • 受控字符序列(controlled character sequence):又称缓冲序列(buffer sequence),由读取区(get area)和/或写入区(put area)组成。此二者分别用来缓冲上层流的读写操作。
  • 关联字符序列(associated character sequence):对于输入流来说又称源(source),对于输出流来说又称槽(sink)。关联字符序列通常是通过系统 API 与 I/O 设备关联,或是与 std::vector/array/字符串字面值等能作为源或槽的对象关联。

对于关联字符序列来说,需要 std::basic_streambuf 自己实现的功能不多。因为,大多数情况可通过系统 API 或是相关对象的接口来实现。std::basic_streambuf 大多数的功能集中在对受控字符序列的管理上。

读取区或写入区,通常实现为相应 CharT 的 C 风格数组,并辅以 3 个指针,以实现对受控字符序列的控制:

  • 起始指针(beginning pointer):用于标识相应缓冲序列可用范围的起始位置;
  • 终止指针(end pointer):用于标识相应缓冲序列可用范围的尾后位置;
  • 工作指针(next pointer):指向相应缓冲序列中,下一个等待读/写的元素的位置。

若是一个受控字符序列单单是读取区或写入区,则它必然有这三个指针;若一个受控字符序列同时是读取区和写入区,那么则有两套共六个这样的指针。通过这些指针,std::basic_streambuf 就能实现对换受控字符序列的控制。

流中的缓冲区

在头文件 ios 当中,定义着两个类(模板):std::ios_base 和 std::basic_ios。前者是所有 I/O 类的祖先,提供了状态信息、控制信息、内部存储、回调等设施。后者继承自前者,额外提供了与 std::basic_streambuf 的接口;同时允许多个 std::basic_ios 对象绑定同一个 std::basic_streambuf 对象。它们的声明分别是:

class ios_base;  
template<  
class CharT,  
class Traits = std::char_traits<CharT>  
> class basic_ios; // : public ios_base

由于 std::ios_base 没有提供与 std::basic_streambuf 的接口,std::basic_ios 才是标准库内所有 I/O 类(模板)事实上的最近共同祖先。std::basic_ios 的成员函数 rdbuf 是读取和设置流对象(std::basic_ios 的对象)绑定缓冲区的成员函数,它有两个不同的重载形式,分别如下:

std::basic_streambuf<CharT, Traits>*
rdbuf() const;                                      // 1.
std::basic_streambuf<CharT, Traits>*
rdbuf( std::basic_streambuf<CharT, Traits>* sb );   // 2.

两个重载版本,第一版不接受任何参数,第二版接受一个指向 std::basic_streambuf<CharT, Traits> 类型对象的指针。

不接受参数的版本返回流对象绑定的缓冲区对象的指针;而若流对象未绑定任何缓冲区对象,则返回空指针 nullptr。接受指针的版本首先返回上述指针,而后与先前绑定的缓冲区对象(如果有)解绑,再绑定参数中传入指针指向的缓冲区对象;而若传入空指针 nullptr,则流对象不与任何缓冲区对象绑定。