如何创建在 C++ 中处理输入和输出的流?

How to create stream which handles both input and output in C++?

我正在尝试制作一个既是输入流又是输出流的 class(例如 std::coutstd::cin )。我试图重载运算符 <<>>,但是后来,我明白编写这样的代码并不明智(因为这将是一种重写 C++ 流的方法)并且当 class 像 std::basic_iostreamstd::basic_ostreamstd::basic_istream 这样的东西在 C++ 标准库中可用,因为我必须为每种类型重载运算符。所以,我试着这样定义我的 class:

#include <istream>

class MyStream : public std::basic_iostream<char> {
public:
    MyStream() : std::basic_iostream<char>(stream_buffer) {}
};

我的问题是 std::basic_iostream<char> 的构造函数的第一个参数。从 cppreference 开始,std::basic_iostream::basic_iostream 接受一个指向从 std::basic_streambuf 派生的流缓冲区的指针:

explicit basic_iostream( std::basic_streambuf<CharT,Traits>* sb );

我已经阅读并尝试了 Apache C++ Standard Library User's Guide's chapter 38 中的示例。它说我必须传递一个指向流缓冲区的指针,有三种方法可以这样做:

最后一个选项最适合我的目的,但是如果我直接从 std::basic_streambuf class 创建一个对象,它什么都不做,对吗?所以我定义了另一个 class 派生自 std::basic_streambuf<char>。但是这次我没看明白到底定义了什么函数,因为不知道插入、提取、刷新数据时调用了哪个函数。

如何创建具有自定义功能的流?


请注意,这是试图建立一个关于创建 C++ 流和流缓冲区的标准指南。

创建一个行为类似于流的 class 很容易。假设我们要创建名称为 MyStream 的 class , class 的定义将很简单:

#include <istream> // class "basic_iostream" is defined here

class MyStream : public std::basic_iostream<char> {
private:
    std::basic_streambuf buffer; // your streambuf object
public:
    MyStream() : std::basic_iostream<char>(&buffer) {} // note that ampersand
};

您的 class 的构造函数应该使用指向自定义 std::basic_streambuf<char> 对象的 指针 调用 std::basic_iostream<char> 的构造函数。 std::basic_streambuf 只是一个模板 class ,它定义了流缓冲区的结构。所以你必须得到你自己的流缓冲区。您可以通过两种方式获得:

  1. 来自另一个流:每个流都有一个成员rdbuf,它不带任何参数,return是一个指向它所使用的流缓冲区的指针。示例:
...
std::basic_streambuf* buffer = std::cout.rdbuf(); // take from std::cout
...
  1. 创建您自己的:您始终可以通过派生自 std::basic_streambuf<char> 创建缓冲区 class 并根据需要自定义它。

现在我们定义并实现了MyStream class,我们需要流缓冲区。让我们从上面的 select 选项 2 创建我们自己的流缓冲区并将其命名为 MyBuffer 。我们需要以下内容:

  1. 构造函数初始化对象。
  2. 程序临时存储输出的连续内存块
  3. 用于临时存储来自用户(或其他)的输入的连续内存块。
  4. 方法overflow,当分配的用于存储输出的内存已满时调用。
  5. 方法 underflow ,在程序读取所有输入并请求更多输入时调用。
  6. 方法sync,当输出刷新时调用。

我们知道创建流缓冲区需要什么东西class,让我们声明它:

class MyBuffer : public std::basic_streambuf<char> {
private:
    char inbuf[10];
    char outbuf[10];

    int sync();
    int_type overflow(int_type ch);
    int_type underflow();
public:
    MyBuffer();
};

这里inbufoutbuf是两个数组,分别存放输入和输出。 int_type 是一种特殊类型,类似于 char 并创建以支持多种字符类型,如 charwchar_t

在我们开始实施我们的缓冲区 class 之前,我们需要知道缓冲区将如何工作。

要了解缓冲区的工作原理,我们需要了解数组的工作原理。数组没什么特别的,只是指向连续内存的指针。当我们声明一个包含两个元素的 char 数组时,操作系统会为我们的程序分配 2 * sizeof(char) 内存。当我们使用 array[n] 访问数组中的元素时,它会转换为 *(array + n) ,其中 n 是索引号。当您将 n 添加到数组时,它会跳转到下一个 n * sizeof(<the_type_the_array_points_to>)(图 1)。如果您不知道什么是指针算法,我建议您在继续之前学习一下。 cplusplus.com has a good article 给初学者的指点。

             array    array + 1
               \        /
------------------------------------------
  |     |     | 'a' | 'b' |     |     |
------------------------------------------
    ...   105   106   107   108   ...
                 |     |
                 -------
                    |
            memory allocated by the operating system

                     figure 1: memory address of an array

由于我们现在对指针了解很多,让我们看看流缓冲区是如何工作的。我们的缓冲区包含两个数组 inbufoutbuf 。但是标准库如何知道输入必须存储到 inbuf 并且输出必须存储到 outbuf ?所以,这里有两个区域,叫做get区和put区,分别是输入区和输出区。

Put区域由以下三个指针指定(图2):

  • pbase()put base: put area 的开始
  • epptr() or end put pointer: put 区结束
  • pptr() or put pointer: 下一个字符的位置

这些实际上是return对应指针的函数。这些指针由 setp(pbase, epptr) 设置。在这个函数调用之后, pptr() 被设置为 pbase() 。要更改它,我们将使用 pbump(n)pptr() 重新定位 n 个字符,n 可以是正数或负数。请注意,流将写入 epptr() 的前一个内存块,但不会写入 epptr() .

  pbase()                         pptr()                       epptr()
     |                              |                             |
------------------------------------------------------------------------
  | 'H' | 'e' | 'l' | 'l' | 'o'  |     |     |     |     |     |     |
------------------------------------------------------------------------
     |                                                      |
     --------------------------------------------------------
                                 |
                   allocated memory for the buffer

           figure 2: output buffer (put area) with sample data

获取区域由以下三个指针指定(图3):

  • eback()返回,获取区域开始
  • egptr() or end get pointer, end of get area
  • gptr()获取指针,要读取的位置

这些指针是用setg(eback, gptr, egptr)函数设置的。请注意,流将读取 egptr() 的前一个内存块,但不会读取 egptr().

  eback()                         gptr()                       egptr()
     |                              |                             |
------------------------------------------------------------------------
  | 'H' | 'e' | 'l' | 'l' | 'o'  | ' ' | 'C' | '+' | '+' |     |     |
------------------------------------------------------------------------
     |                                                      |
     --------------------------------------------------------
                                 |
                   allocated memory for the buffer

           figure 3: input buffer (get area) with sample data

现在我们已经讨论了几乎所有我们在创建自定义流缓冲区之前需要了解的内容,是时候实现它了!我们将尝试实现我们的流缓冲区,使其像 std::cout !

一样工作

让我们从构造函数开始:

MyBuffer() {
    setg(inbuf+4, inbuf+4, inbuf+4);
    setp(outbuf, outbuf+9);
}

这里我们将所有三个get指针都设置到一个位置,这意味着没有可读字符,在需要输入时强制underflow()。然后我们以这种方式设置 put 指针,以便流可以写入除最后一个元素之外的整个 outbuf 数组。我们会保留它以备将来使用。

现在,让我们实现 sync() 方法,该方法在刷新输出时调用:

int sync() {
    int return_code = 0;

    for (int i = 0; i < (pptr() - pbase()); i++) {
        if (std::putchar(outbuf[i]) == EOF) {
            return_code = EOF;
            break;
        }
    }

    pbump(pbase() - pptr());
    return return_code;
}

这很容易做到。首先,它确定要打印多少个字符然后一张一张打印并重新定位 pptr() (放置指针)。它 returns EOF 或 -1 如果字符任何字符是 EOF,否则为 0。

但是放置区满了怎么办?所以,我们需要 overflow() 方法。让我们来实现它:

int_type overflow(int_type ch) {
    *pptr() = ch;
    pbump(1);

    return (sync() == EOF ? EOF : ch);
}

不是很特别,只是将多余的字符放入outbuf保留的最后一个元素中,重新定位pptr()(放置指针),然后调用sync()。它 returns EOF if sync() returned EOF,否则多余的字符。

现在一切都完成了,除了输入处理。让我们实现 underflow() ,当读取输入缓冲区中的所有字符时调用它:

int_type underflow() {
    int keep = std::max(long(4), (gptr() - eback()));
    std::memmove(inbuf + 4 - keep, gptr() - keep, keep);

    int ch, position = 4;
    while ((ch = std::getchar()) != EOF && position <= 10) {
        inbuf[position++] = char(ch);
        read++;
    }
    
    if (read == 0) return EOF;
    setg(inbuf - keep + 4, inbuf + 4 , inbuf + position);
    return *gptr();
}

有点难懂。让我们看看这里发生了什么。首先,它计算应该在缓冲区中保留多少个字符(最多 4 个)并将其存储在 keep 变量中。然后它将最后 keep 个字符复制到缓冲区的开头。这样做是因为可以使用 std::basic_iostreamunget() 方法将字符放回缓冲区。程序甚至可以读取下一个字符,而无需使用 std::basic_iostreampeek() 方法提取它。放回最后几个字符后,它会读取新字符,直到到达输入缓冲区的末尾或获取 EOF 作为输入。如果没有读取到字符,则 returns EOF,否则继续。然后它重新定位所有获取指针和 return 读取的第一个字符。

由于我们的流缓冲区现在已实现,我们可以设置我们的流 class MyStream 以便它使用我们的流缓冲区。所以我们更改私有 buffer 变量:

...
private:
    MyBuffer buffer;
public:
...

您现在可以测试您自己的流,它应该从终端获取输入并显示输出。


请注意,此流和缓冲区只能处理基于 char 的输入和输出。您的 class 必须派生自相应的 class 以处理其他类型的输入和输出(例如 std::basic_streambuf<wchar_t> 用于宽字符)并实现成员函数或方法以便它们可以处理该类型的字符。