C++笔记:std::stringbuf

借用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 类:

  1. 构造 / 模式控制接口

  2. string 专属接口(核心扩展)

  3. 来自 streambuf 的读写接口(sgetc,sputc 等)

  4. 来自 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 的扩展接口(最重要!)

  1. str() --- 获取内容
cpp 复制代码
std::string s = buf.str();

把内部的缓冲字符串得到一份复制。

  1. 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::endlflush、析构 flush、异常 flush 会调用 sync()

我们通常这样用:

cpp 复制代码
os << "hello" << std::endl;

endl 做了两件事:

  1. 输出 '\n'

  2. 调用 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会有更深的理解

相关推荐
Rhys..1 小时前
Jenkinsfile保存在项目根目录下的好处
java·开发语言
lly2024061 小时前
SQL LCASE() 函数详解
开发语言
0***K8921 小时前
PHP框架比较
开发语言·php
哟哟耶耶1 小时前
ts-属性修饰符,接口(约束数据结构),抽象类(约束与复用逻辑)
开发语言·前端·javascript
nvd112 小时前
Gidgethub 使用指南
开发语言·python
讨厌下雨的天空2 小时前
线程同步与互斥
java·开发语言
娶不到胡一菲的汪大东2 小时前
C# 泛型 委托 接口
开发语言·windows·c#
Antonio9152 小时前
【Swift】UIKit:UISegmentedControl、UISlider、UIStepper、UITableView和UICollectionView
开发语言·ios·swift
0***142 小时前
JavaScript视频处理案例
开发语言·javascript·音视频