在c++程序开发中,我们经常会遇到需要把输出内容同时显示在控制台和保存到文件中的场景,比如程序运行日志需要实时查看也要持久化存储。传统的做法是每次输出时同时调用cout和文件写入接口,不仅代码重复,还容易出现遗漏。自定义TeeStream流可以优雅地解决这个问题,让输出操作像使用普通输出流一样简单,同时完成屏幕打印和文件写入两个任务。

自定义TeeStream流的核心思路
TeeStream的核心原理是继承c++的输出流基类,重写内部的输出相关方法,让每一次输出操作同时触发两个输出目标的处理。c++的标准输出流体系里,<ostream>是所有输出流的基础,我们可以基于它实现一个自定义的流类,内部维护两个输出目标:一个是标准输出cout关联的流缓冲区,另一个是打开的文件对应的流缓冲区。
当向TeeStream写入数据时,我们会把数据同时发送到这两个流缓冲区,这样就能实现一份输出同时到达屏幕和文件的效果。这种方式对使用者完全透明,调用方不需要关心底层是两个目标还是单个目标,只需要像使用普通<ostream>对象一样操作即可。
完整实现代码
下面是自定义TeeStream流的完整实现代码,包含头文件引用、类定义和核心方法实现:
#include <iostream>
#include <fstream>
#include <streambuf>
#include <string>
// 自定义流缓冲区,用于将输出同步到两个目标
class TeeStreamBuf : public std::streambuf {
private:
std::streambuf* screen_buf; // 屏幕输出对应的流缓冲区
std::streambuf* file_buf; // 文件输出对应的流缓冲区
public:
TeeStreamBuf(std::streambuf* screen, std::streambuf* file)
: screen_buf(screen), file_buf(file) {}
protected:
// 重写overflow方法,处理单个字符的输出
int overflow(int c) override {
if (c == EOF) {
return EOF;
}
// 先写入屏幕缓冲区
if (screen_buf->sputc(c) == EOF) {
return EOF;
}
// 再写入文件缓冲区
if (file_buf->sputc(c) == EOF) {
return EOF;
}
return c;
}
// 重写xsputn方法,处理批量字符的输出,提升效率
std::streamsize xsputn(const char* s, std::streamsize n) override {
// 先写入屏幕缓冲区
std::streamsize screen_written = screen_buf->sputn(s, n);
// 再写入文件缓冲区
std::streamsize file_written = file_buf->sputn(s, n);
// 返回实际写入的长度,这里取最小值作为结果
return screen_written < file_written ? screen_written : file_written;
}
};
// 自定义TeeStream输出流类
class TeeStream : public std::ostream {
private:
TeeStreamBuf tee_buf; // 自定义的流缓冲区对象
std::ofstream file_stream; // 文件输出流对象
public:
TeeStream(const std::string& file_path)
: tee_buf(std::cout.rdbuf(), nullptr), std::ostream(&tee_buf) {
// 打开文件,设置为追加模式,文本格式
file_stream.open(file_path, std::ios::out | std::ios::app);
if (!file_stream.is_open()) {
std::cerr << "无法打开文件: " << file_path << std::endl;
return;
}
// 将文件流缓冲区设置到自定义缓冲区中
tee_buf = TeeStreamBuf(std::cout.rdbuf(), file_stream.rdbuf());
}
~TeeStream() {
// 关闭文件流
if (file_stream.is_open()) {
file_stream.close();
}
}
};
使用示例
实现好TeeStream类之后,我们可以像使用普通输出流一样使用它,下面是完整的使用示例:
#include <iostream>
#include "TeeStream.h" // 假设上面的实现代码放在TeeStream.h中
int main() {
// 创建TeeStream对象,指定要写入的文件路径
TeeStream tee("program_log.txt");
// 检查文件是否打开成功
if (!tee) {
std::cerr << "创建TeeStream失败" << std::endl;
return 1;
}
// 像使用cout一样使用tee对象输出内容
tee << "程序启动时间: 2024-05-20 10:30:00" << std::endl;
tee << "当前运行参数: mode=debug, port=8080" << std::endl;
int a = 10, b = 20;
tee << "计算结果: " << a << " + " << b << " = " << (a + b) << std::endl;
// 格式化输出同样支持
tee << std::hex << "十六进制数值: " << 255 << std::endl;
tee << std::dec; // 恢复十进制输出
tee << "程序正常结束" << std::endl;
return 0;
}
实现注意事项
- 流缓冲区的生命周期管理:自定义流缓冲区对象需要在输出流对象之前销毁,否则可能出现访问野指针的问题,上面的实现中TeeStreamBuf作为TeeStream的成员变量,生命周期和TeeStream一致,避免了这个问题。
- 文件打开模式:如果希望每次运行程序都清空之前的日志,可以将打开文件的模式改为<std::ios::out>,去掉<std::ios::app>即可,根据实际需求调整。
- 错误处理:文件打开失败时需要有对应的处理逻辑,上面的实现中如果文件打开失败,只会输出错误信息到cerr,实际使用时可以根据需求调整错误处理方式。
- 性能优化:批量输出时重写的<xsputn>方法可以减少多次调用<overflow>的开销,对于大量输出内容的场景,能显著提升性能。
适用场景
这种自定义TeeStream的方式非常适合需要同步输出到屏幕和文件的场景,比如程序运行日志、调试信息输出、数据采集程序的实时展示和存储等。相比手动分别调用cout和文件写入的方式,代码更简洁,维护成本更低,也不容易出现输出遗漏的问题。