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

标签 c++ iostream c++-faq streambuf

我正在尝试创建一个既是输入流又是输出流的类(如 std::coutstd::cin )。我试图重载运算符 <<>> ,但后来,我明白编写这样的代码是不明智的(因为这将是一种重写 C++ 流的方法)并且当像 std::basic_iostream 这样的类时维护非常困难。 , std::basic_ostream , std::basic_istream在 C++ 标准库中可用,因为我必须为每种类型重载运算符。所以,我试图这样定义我的类:

#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 的示例.它说我必须传递一个指向流缓冲区的指针,有三种方法可以这样做:
  • 在类初始化之前创建流缓冲区
  • 从另一个流中获取流缓冲区(使用 rdbuf() 或类似成员)
  • 定义一个 basic_streambuf对象作为 protected 或私有(private)成员

  • 最后一个选项最适合我的目的,但如果我直接从 std::basic_streambuf 创建一个对象类,它什么都不做,是吗?所以我定义了另一个派生自 std::basic_streambuf<char> 的类.但是这次我无法理解要定义哪些函数,因为我不知道在插入,提取和刷新数据时调用了哪个函数。
    如何创建具有自定义功能的流?

    请注意,这是尝试构建有关创建 C++ 流和流缓冲区的标准指南。

    最佳答案

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

    #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
    };
    
    你的类的构造函数应该调用 std::basic_iostream<char> 的构造函数与 指针到定制 std::basic_streambuf<char>目的。 std::basic_streambuf只是一个定义流缓冲区结构的模板类。所以你必须得到你自己的流缓冲区。您可以通过两种方式获得它:
  • 来自另一个流:每个流都有一个成员 rdbuf它不接受任何参数并返回一个指向它正在使用的流缓冲区的指针。示例:

  • ...
    std::basic_streambuf* buffer = std::cout.rdbuf(); // take from std::cout
    ...
    
  • 创建您自己的:您始终可以通过从 std::basic_streambuf<char> 派生来创建缓冲区类。并根据需要对其进行自定义。

  • 现在我们定义并实现了 MyStream类,我们需要流缓冲区。让我们从上面选择选项 2 并创建我们自己的流缓冲区并将其命名为 MyBuffer .我们将需要以下内容:
  • 构造函数来初始化对象。
  • 用于存储输出的连续内存块 暂时按程序。
  • 用于存储输入的连续内存块 暂时来自用户(或其他人)。
  • 方法overflow ,当用于存储输出的分配内存已满时调用。
  • 方法underflow ,当程序读取所有输入并请求更多输入时调用。
  • 方法sync ,在刷新输出时调用。

  • 我们知道创建流缓冲区类需要什么,让我们声明它:
    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 并创建以支持多种字符类型,如 char , wchar_t , 等等。
    在我们进入缓冲区类的实现之前,我们需要知道缓冲区是如何工作的。
    要了解缓冲区的工作原理,我们需要了解数组的工作原理。数组没什么特别的,只是指向连续内存的指针。当我们声明一个 char有两个元素的数组,操作系统分配2 * sizeof(char)我们程序的内存。当我们使用 array[n] 访问数组中的元素时, 转换为 *(array + n) ,其中 n是索引号。当您添加 n到一个数组,它跳到下一个 n * sizeof(<the_type_the_array_points_to>) (图1)。如果您不知道什么指针算术,我建议您在继续之前学习。 cplusplus.com有一个 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 ?因此,有两个区域称为获取区域和放置区域,分别是输入和输出区域。
    Put区域由以下三个指针指定(图2):
  • pbase() 把基地 : 放置区开始
  • epptr() 结束放置指针 : 放置区结束
  • pptr() 放置指针 : 下一个字符的位置

  • 这些实际上是返回相应指针的函数。这些指针由 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() 结束获取指针 , 获取区域结束
  • 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() (放置指针)。如果字符任何字符是 EOF,则返回 EOF 或 -1,否则返回 0。
    但是如果放置区已满怎么办?所以,我们需要 overflow()方法。让我们实现它:
    int_type overflow(int_type ch) {
        *pptr() = ch;
        pbump(1);
    
        return (sync() == EOF ? EOF : ch);
    }
    
    不是很特别,这只是将额外的字符放入 outbuf 的保留最后一个元素中和重新定位 pptr() (放置指针),然后调用 sync() .如果 sync() 返回 EOF返回 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数字字符到缓冲区的开头。这样做是因为可以使用 unget() 将字符放回缓冲区。 std::basic_iostream的方法.程序甚至可以读取下一个字符而无需使用 peek() 提取它std::basic_iostream的方法.放回最后几个字符后,它会读取新字符,直到到达输入缓冲区的末尾或获取 EOF 作为输入。如果没有读取任何字符,则返回 EOF,否则继续。然后它重新定位所有获取指针并返回读取的第一个字符。
    由于我们的流缓冲区现在已经实现,我们可以设置我们的流类 MyStream所以它使用我们的流缓冲区。所以我们改变了私有(private) buffer多变的:
    ...
    private:
        MyBuffer buffer;
    public:
    ...
    
    您现在可以测试自己的流,它应该从终端获取输入并显示输出。

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

    关于c++ - 如何在 C++ 中创建处理输入和输出的流?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63034484/

    相关文章:

    c++ - 虚拟运算符 float()

    c++ - C++ 内联友元函数会导致命名空间之间隐藏名称吗?

    c++ - 自定义迭代器上的前缀和后缀++做同样的事情

    c++ - 使用 std::ifstream,忽略字符和查找之间有区别吗?

    c++ - 将文件加载到 vector <unsigned char>中

    c++ - 什么是 rvalues、lvalues、xvalues、glvalues 和 prvalues?

    c++ - 如何在caffe中提取图层的blob信息?

    c++ - Xcode 11.1 : iostream' file not found

    c++ - 运算符重载的基本规则和惯用法是什么?

    c++ - 什么是 undefined reference /未解析的外部符号错误以及如何修复它?