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

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

提问人:Akib Azmain Turja 提问时间:7/22/2020 最后编辑:Akib Azmain Turja 更新时间:12/7/2020 访问量:2351

问:

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

#include <istream>

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

我的问题是构造函数的第一个参数。从 cppreference 开始,采用指向派生自 :std::basic_iostream<char>std::basic_iostream::basic_iostreamstd::basic_streambuf

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

我已经阅读并尝试了Apache C++标准库用户指南第38章中的示例。它说我必须将指针传递到流缓冲区,有三种方法可以做到这一点:

  • 在类初始化之前创建流缓冲区
  • 从另一个流(using 或类似成员)获取流缓冲区rdbuf()
  • 将对象定义为受保护成员或私有成员basic_streambuf

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

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


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

C iostream C++-FAQ 流布

评论

4赞 john 7/22/2020
你有正确的想法。basic_streambuf不执行任何操作,因此您需要从执行所需操作的basic_streambuf派生一个类。但这是一个需要在这里回答的话题。有一本好涵盖了这个话题。或者你可以抓住机会,用谷歌搜索。
1赞 user253751 7/22/2020
@john 我不认为问如何实现 streambuf 太大而无法在这里回答。它太大了,我现在无法在这里回答,但它可以适合答案格式。
0赞 Antonio 11/12/2023
以下是有关如何将 LCD 用作 std::ostream: github.com/amanuellperez/mcu/blob/master/src/devices/hwd/....关于如何实现iostream最好的手册是标准(问题是标准不是简单的讲授)
0赞 Antonio 11/12/2023
一个更复杂的示例是将 UART 用作 std::iostream: github.com/amanuellperez/mcu/blob/master/src/microcontrollers/...。对不起,部分内容是西班牙语,但对标准的引用都是英文的。

答:

20赞 Akib Azmain Turja 8/26/2020 #1

创建一个行为类似于流的类很容易。假设我们想创建这样的类,名称为,类的定义将很简单: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

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

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

  1. 用于初始化对象的构造函数
  2. 连续存储器块,用于临时存储程序输出
  3. 连续内存块,用于临时存储来自用户(或其他东西)的输入。
  4. 方法溢出,当为存储输出分配的内存已满时调用。
  5. 方法下溢 ,当程序读取所有输入并请求更多输入时调用。
  6. 方法 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();
};

这里 和 是两个数组,它们将分别存储输入和输出。 是一种特殊类型,类似于 char,旨在支持多种字符类型,如 、 等。inbufoutbufint_typecharwchar_t

在我们进入缓冲区类的实现之前,我们需要知道缓冲区将如何工作。

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

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

                     figure 1: memory address of an array

由于我们现在对指针了解很多,让我们看看流缓冲区是如何工作的。我们的缓冲区包含两个数组和 .但是标准库如何知道输入必须存储到,输出必须存储到?因此,有两个区域称为获取区域和放置区域,分别是输入和输出区域。inbufoutbufinbufoutbuf

用以下三个指针指定放置区域(图 2):

  • pbase()put base:看跌区域的开始
  • epptr()end put 指针:put 区域结束
  • pptr()put 指针:下一个字符的放置位置

这些实际上是返回相应指针的函数。这些指针由 设置。调用此函数后,设置为 。为了改变它,我们将使用 n 个字符的重新定位,n 可以是正数或负数。请注意,流将写入 的上一个内存块,但不会写入 。setp(pbase, epptr)pptr()pbase()pbump(n)pptr()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()end back,get 区域的开头
  • egptr() 或结束获取指针,获取区域结束
  • gptr()get 指针,将要读取的位置

这些指针是用函数设置的。请注意,流将读取 的上一个内存块,但不会读取 。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 指针都设置到一个位置,这意味着没有可读字符,在需要输入时强制执行。然后我们以这样的方式设置 put 指针,以便流可以写入除最后一个元素之外的整个数组。我们将保留它以备将来使用。underflow()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;
}

这很容易工作。首先,它确定要打印的字符数,然后逐个打印并重新定位(放置指针)。如果字符任何字符为 EOF,则返回 EOF 或 -1,否则返回 0。pptr()

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

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

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

不是很特别,这只是将多余的字符放入保留的最后一个元素中并重新定位(放置指针),然后调用 .如果返回 EOF,则返回 EOF,否则返回额外字符。outbufpptr()sync()sync()

现在一切都完成了,除了输入处理。让我们实现 ,当读取输入缓冲区中的所有字符时调用: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 个)并将其存储在变量中。然后,它将最后一个数字字符复制到缓冲区的开头。这样做是因为可以使用 的方法将字符放回缓冲区中。程序甚至可以读取下一个字符,而无需使用 的方法提取它。将最后几个字符放回原处后,它会读取新字符,直到到达输入缓冲区的末尾或获取 EOF 作为输入。然后,如果未读取任何字符,则返回 EOF,否则继续。然后,它重新定位所有获取指针并返回读取的第一个字符。keepkeepunget()std::basic_iostreampeek()std::basic_iostream

由于现在实现了流缓冲区,我们可以设置流类,以便它使用流缓冲区。因此,我们更改私有变量:MyStreambuffer

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

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


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

评论

0赞 Tanveer Badar 10/5/2020
惊人的解释。不过有一个小建议,如果可读字符全部位于指针的右侧而不是左侧,则获取输入示例会更好。