借用c++ 流对象之streambuf(可当做缓冲区使用) - 大老虎打老虎 - 博客园 这篇博客的图,我们可以梳理一下C++流缓冲部分的关系图

同时对比一下源码(MSVC)
cpp
_EXPORT_STD using ios = basic_ios<char, char_traits<char>>;
_EXPORT_STD using streambuf = basic_streambuf<char, char_traits<char>>;
_EXPORT_STD using istream = basic_istream<char, char_traits<char>>;
_EXPORT_STD using ostream = basic_ostream<char, char_traits<char>>;
_EXPORT_STD using iostream = basic_iostream<char, char_traits<char>>;
_EXPORT_STD using stringbuf = basic_stringbuf<char, char_traits<char>, allocator<char>>;
_EXPORT_STD using istringstream = basic_istringstream<char, char_traits<char>, allocator<char>>;
_EXPORT_STD using ostringstream = basic_ostringstream<char, char_traits<char>, allocator<char>>;
_EXPORT_STD using stringstream = basic_stringstream<char, char_traits<char>, allocator<char>>;
_EXPORT_STD using filebuf = basic_filebuf<char, char_traits<char>>;
_EXPORT_STD using ifstream = basic_ifstream<char, char_traits<char>>;
_EXPORT_STD using ofstream = basic_ofstream<char, char_traits<char>>;
_EXPORT_STD using fstream = basic_fstream<char, char_traits<char>>;
本文主要从streambuf到stringbuf这一顺序介绍下来,io部分和文件部分暂且略过。
basic_streambuf
cpp
template <class _Elem, class _Traits>
class basic_streambuf;
直观理解
basic_streambuf 是流的内核"。
它不直接负责格式化输入输出,而是负责 字符缓冲区(buffer)的管理。
上层的 istream / ostream 只是包装它,提供了提取(>>)和插入(<<)等语法糖。
文件 / 字符串 / 网络
↓
streambuf (管理原始字节、缓冲、指针移动)
↓
basic_istream / basic_ostream (负责格式化、重载 << >>)
↓
用户代码
成员变量结构(核心缓冲机制)
代码里的成员变量很多,但其实逻辑上就分两大块:
读缓冲区(get area) 和 写缓冲区(put area)。
由于MSVC,GCC对于缓冲区实现机制不一样,这里就关注接口和设计了。
输入区(get area)3 个指针
cpp
eback() ---- start pointer
gptr() ---- current read pointer
egptr() ---- end pointer
| 指针 | 含义 |
|---|---|
eback() |
读缓冲区的起始位置 |
gptr() |
当前读取到的位置 |
egptr() |
输入缓冲区的结束位置 |
输出区(put area)3 个指针
cpp
pbase() ---- start pointer
pptr() ---- current write pointer
epptr() ---- end pointer
| 指针 | 含义 |
|---|---|
pbase() |
写缓冲区的起始位置 |
pptr() |
当前写入位置 |
epptr() |
写缓冲区的结束位置 |
streambuf 的核心行为(跨平台一致)
1. 输入(读)流程:
istream 调用 sbumpc() 或 sgetc() 时:
cpp
if (gptr() < egptr())
返回 *gptr(), gptr()++
else
调用 underflow()
👉 underflow() 是由子类实现的数据补给点
2. 输出(写)流程:
ostream 调用 sputc(ch) 时:
cpp
if (pptr() < epptr())
*pptr()++ = ch
else
调用 overflow(ch)
👉 overflow() 是由子类处理写满缓冲的情况
3. seek(移动)和 sync(同步)
streambuf 提供虚函数:
-
sync():把缓冲区刷新到目标(文件等) -
seekoff()、seekpos():移动读/写指针位置
这些都由子类如 filebuf 实现。
跨平台统一的 streambuf 本质
无论 MSVC / GCC / Clang,它们都必须遵守以下规则:
streambuf 本质 = 一对 get/put 缓冲区 + 若干虚函数
6 个标准指针模型是强制的(only names differ)
iostream 不直接访问内部成员,只通过指针接口:
-
gptr()→ 当前读位置 -
egptr()→ 读缓冲上限 -
pptr()→ 当前写位置 -
epptr()→ 写缓冲上限
所有流的读写逻辑都靠 overflow()/underflow() 回调处理
派生类(filebuf/stringbuf)负责真正的 I/O
stringbuf
stringbuf 相比 streambuf 做了哪些扩展?
(1) 内部缓冲区是一个 std::string
streambuf 不知道数据来自哪里,也不知道放到哪里 ------ 文件、socket、内存都可能。
stringbuf 则明确:
👉 它的读写缓冲区 = 一个 std::string
并且 get/put 区域都指向这同一个字符串,因此:
-
你写入内容 → 会追加到 string 里
-
你读取内容 → 就是从这个 string 中读出
这是最重要的扩展。
(2) 实现了 underflow() / overflow()
streambuf 默认这些函数行为都是 "失败"。
stringbuf 实现了:
| 函数 | stringbuf 的作用 |
|---|---|
overflow() |
写太多时自动扩展 string |
underflow() |
从 string 中补充读取字符 |
seekoff/seekpos |
在 string 中移动读写指针 |
也就是说:
👉 stringbuf 自己就可以当成可扩展内存 buffer 使用
支持三种 open mode(重要)
stringbuf 有类似 fstream 的三种模式:
-
in:允许读取
-
out:允许写入
-
ate:创建时指向末尾
cpp
std::stringbuf buf(std::ios::in | std::ios::out);
(4) 可以直接提取或替换内部字符串
cpp
buf.str(); // 取出当前字符串
buf.str("abc"); // 设置内部字符串为 "abc"
stringbuf 的常用 API 分析概览
stringbuf 的 API 可以分 4 类:
-
构造 / 模式控制接口
-
string 专属接口(核心扩展)
-
来自 streambuf 的读写接口(sgetc,sputc 等)
-
来自 streambuf 的指针定位接口(seekoff/seekpos)
下面逐个解释。
构造函数与模式控制(in/out/ate)
cpp
std::stringbuf buf; // 默认 out 模式
std::stringbuf buf(std::ios::in); // 只读
std::stringbuf buf(std::ios::out); // 只写
std::stringbuf buf(std::ios::in | std::ios::out); // 可读可写
std::stringbuf buf(std::ios::in | std::ios::out | std::ios::ate);
ate = 初始指针位置在末尾
它们表示 stringbuf 的 get/put 区域如何工作。
stringbuf 的扩展接口(最重要!)
- str() --- 获取内容
cpp
std::string s = buf.str();
把内部的缓冲字符串得到一份复制。
- str(const string&) --- 设置内容
cpp
buf.str("hello");
-
这会替换内部字符串
-
自动重新设置 get/put 指针
来自 streambuf 的字符读写接口
输入(get 区域)API
| 函数 | 作用 |
|---|---|
| sgetc() | 获取当前字符(不移动指针) |
| snextc() | 前进 1 并返回新位置字符 |
| sbumpc() | 返回当前字符 + 指针前进 |
| *sgetn(char s, std::streamsize n)** | 批量读取 n 个字节 |
| underflow() | 底层 refill(stringbuf 已定义) |
cpp
char c = buf.sgetc(); // peek
char c2 = buf.sbumpc(); // read and move
输出(put 区域)API
| 函数 | 作用 |
|---|---|
| sputc(char) | 写入一个字符 |
| *sputn(const char, size_type)** | 批量写入 |
| overflow() | buffer 写满时扩容(stringbuf 已定义) |
cpp
buf.sputc('H');
buf.sputn("ello", 4);
stringbuf 的 overflow() 会自动扩容字符串,所以 sputc 永远不会失败(除非内存不足)。
指针移动(seek)
stringbuf 重写 了 seekoff 和 seekpos
(因为它必须根据内部 string 来移动指针)
seekoff
cpp
buf.pubseekoff(off, dir, mode);
参数:
-
off:偏移值(正负)
-
dir:
-
std::ios::beg -
std::ios::cur -
std::ios::end
-
-
mode:
-
std::ios::in→ 移动 read 指针 -
std::ios::out→ 移动 write 指针
-
cpp
buf.pubseekoff(0, std::ios::beg, std::ios::in); // 读指针归零
buf.pubseekoff(0, std::ios::end, std::ios::out); // 写指针跳到末尾
seekpos
cpp
buf.pubseekpos(pos, mode);
直接移动到绝对位置。
sync()何时重写?
在stringbuf里,sync返回0;但是有时我们继承stringbuf自定义缓冲,需要重写sync()
sync() 是 std::basic_streambuf 中的 虚函数,用来:
👉 将 put 区中未写入底层的数据"同步"到底层设备
👉 刷新缓冲区
和 std::ostream::flush() 最终调用的是同一个东西。
返回值:
-
0 → 成功
-
-1 → 失败
std::endl、flush、析构 flush、异常 flush 会调用 sync()。
我们通常这样用:
cpp
os << "hello" << std::endl;
endl 做了两件事:
-
输出
'\n' -
调用
os.flush()→ 调用pubsync()→ 调用sync()
cpp
os << std::endl;
实际发生:
cpp
endl → os.put('\n') → os.flush()
→ pubsync()
→ sync() // 你重写的函数执行
这在我们把stringbuf当作输入输出缓冲的时候特别重要。下面会进行介绍。
stringbuf 什么时候使用?
场景 1:你只需要一个可读写的"内存缓冲区"
你不想要完整的 sstream API,只想要一个底层缓冲区:
cpp
std::stringbuf buf;
buf.sputc('H');
buf.sputc('i');
std::cout << buf.str(); // "Hi"
场景 2:你要继承 streambuf 实现自己的类,stringbuf 是参考模板
例如你想实现:
-
从网络读的流(socketbuf)
-
从你自己的数据结构读的流
你可以参考 stringbuf 的实现方式
或者直接继承 stringbuf 修改一部分逻辑。
场景 3:和其他流组合(tie)***
你要把一个 string 当成流的底层缓冲区时:
cpp
std::stringbuf buf;
std::iostream io(&buf);
此时你获得一个:
✔ 同时可读可写
✔ 底层存储是字符串
✔ 有完整的 iostream API 的对象
后面会详细研究iostream绑定一个stringbuf做底层缓冲的情况
场景 4:实现自定义 log 系统
当你需要写格式化输出到内存,而不是 stdout:
cpp
std::stringbuf logbuf;
std::ostream logStream(&logbuf);
logStream << "Error: " << errorCode << "\n";
auto content = logbuf.str();
使用stringbuf实现自定义缓冲区
cpp
#pragma warning (disable:4996)
#include <iostream>
#include <sstream>
using namespace std;
class LogBuffer :public std::stringbuf {
public:
explicit LogBuffer(std::ostream& stream) :mOutput(stream) {}
//拷贝应该是禁止的,我这里偷懒没写了
~LogBuffer()override {//重要,这个得重写
//防止有残留数据留在缓冲区
if (this->pbase() != this->pptr()) {
putOutput();
}
}
//重要,flush,endl会调用sync
int sync()override {
this->putOutput();
return 0;
}
private:
void putOutput() {
mOutput << this->str();
this->str("");//清空
mOutput.flush();
}
std::ostream& mOutput;
};
int main() {
LogBuffer logger(std::cout);
std::ostream myostream(&logger);
myostream << "hello" << std::endl;//会把hello输出
//逻辑是这样的,myostream的<<只会把数据输入到LogBuffer的缓冲区里面,并不会输出到屏幕
//<<std::endl,LogBuffer内部调用sync,std::cout这时候才把缓冲区的内容进行输出
}
理解了这个例子,对于缓冲区,ostream会有更深的理解