直接继承 std::streambuf 是唯一标准、零开销的底层拦截方式,需重写 underflow()、overflow()、sputn()、sgetn() 并正确维护 setg()/setp(),转发 i/o 至原始设备;包装 fstream 或重载 operator 无法截获底层缓冲行为。

如何用 std::streambuf 派生类拦截文件读写
直接继承 std::streambuf 是唯一标准、零开销的底层拦截方式。它不依赖 std::fstream 的高层接口,而是接管缓冲区行为本身——underflow() 控制读、overflow() 和 sputn() 控制写,所有字符级操作都会经过你重写的函数。
关键点在于:不能只重写一个函数就以为能监控全部流量。比如只改 overflow() 会漏掉 write(buf, n) 这种批量写入(它走 sputn()),而 get() 或 >> 可能触发多次 underflow(),但单次调用可能返回多个字符。
- 必须同时重写
underflow()、overflow()、sputn()、sgetn(),否则监控不完整 -
setg()/setp()要正确维护内部指针,否则流状态会变failbit - 原始底层设备(如
std::filebuf)需在构造时保存,所有实际 I/O 都要转发给它
为什么不能包装 std::fstream 对象或重载 operator
包装 std::fstream(比如写个 MonitoredFStream 类,内部持有一个 std::fstream)只能拦截你显式调用的成员函数,对 std::ostream& os 这类泛型参数、模板实例化(如 fmt::print、spdlog 后端)、或标准库内部调用完全无效。重载全局 operator 更不可行——它无法区分目标是否是文件流,且会干扰所有其他流类型,破坏 ADL 和重载解析。
真正起效的拦截必须发生在流缓冲区层级,因为所有 C++ 标准流最终都归结为 streambuf::sputn() / streambuf::sgetn() 调用。这是唯一被标准明确要求由实现调用的底层接口。
- 包装对象:漏掉隐式转换、模板推导、第三方日志库等间接使用场景
- 重载
operator:污染全局命名空间,无法按流对象区分监控策略 - 只有
streambuf派生可被std::ios::rdbuf()安全替换,且不影响原有流接口
std::filebuf 替换后如何保持异常安全与线程安全
用 my_streambuf 替换 std::fstream::rdbuf() 后,原 std::filebuf 实例必须由你管理生命周期。常见错误是让它随 std::fstream 析构自动销毁,导致你的 my_streambuf 持有悬空指针。
线程安全不能依赖流对象自身(std::fstream 不保证多线程并发读写安全),而应由你的 streambuf 在关键计数器(如字节数统计)上加锁。但锁粒度要细:只锁计数更新,不要锁整个 sputn() ——否则会严重拖慢吞吐。
- 原始
std::filebuf*必须 new 出来,或用std::unique_ptr管理,确保寿命长于监控 buf - 避免在
underflow()中抛异常;若底层 read 失败,应设badbit而非 throw - 计数变量用
std::atomic<size_t></size_t>比互斥锁更轻量,适合高频小数据包场景
监控到的字节数为何比预期少?检查这三点
实测发现统计值偏小,大概率不是逻辑错,而是缓冲机制导致的“延迟上报”。例如:
- 写入未满缓冲区就析构流:最后几个字节还卡在你的
streambuf输出缓冲里,没传给底层filebuf,自然不会被计入 - 调用
flush()前未手动同步:C++ 流默认行缓存或全缓存,write()后不flush()就 close,数据会丢失 -
std::ios_base::binary未设置:文本模式下换行符转换(\n ↔ \r\n)会使底层filebuf实际写入字节数 ≠ 你传入的字节数,但你的监控只算输入长度
调试时可在析构函数中强制 sync() 并检查返回值,再 dump 当前缓冲区剩余字节数——这才是真正“漏掉”的部分。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











