创建一个行为类似于流的类很容易。假设我们要创建一个名为 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();
};
这里inbuf 和outbuf 是两个数组,分别存储输入和输出。 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
既然我们现在对指针有了很多了解,让我们看看流缓冲区是如何工作的。我们的缓冲区包含两个数组 inbuf 和 outbuf 。但是标准库如何知道输入必须存储到 inbuf 并且输出必须存储到 outbuf ?所以,有两个区域叫做get area和put area,分别是输入和输出区域。
Put区域用以下三个指针指定(图2):
-
pbase() 或 put base:put 区域的开始
-
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() 或 end back,获取区域的开始
-
egptr() 或 结束获取指针,结束获取区域
-
gptr() 或 get pointer,要读取的位置
这些指针是用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 方法将字符放回缓冲区。程序甚至可以读取下一个字符而无需使用 std::basic_iostream 的 peek() 方法提取它。在放回最后几个字符后,它会读取新字符,直到到达输入缓冲区的末尾或将 EOF 作为输入。如果没有读取字符,则返回 EOF,否则继续。然后它重新定位所有 get 指针并返回读取的第一个字符。
由于我们的流缓冲区现在已经实现,我们可以设置我们的流类MyStream,以便它使用我们的流缓冲区。所以我们改变私有的buffer变量:
...
private:
MyBuffer buffer;
public:
...
您现在可以测试自己的流,它应该从终端获取输入并显示输出。
请注意,此流和缓冲区只能处理基于 char 的输入和输出。您的类必须派生自相应的类以处理其他类型的输入和输出(例如,std::basic_streambuf<wchar_t> 用于宽字符)并实现成员函数或方法,以便它们可以处理该类型的字符。