std::cout不能直接hook因其底层绑定不可变的std::streambuf实例(如std::filebuf),只能通过替换rdbuf()指针或派生重写overflow()/sputn()实现自定义写入。

std::cout 输出为什么不能直接 hook
因为 std::cout 底层绑定的是 std::cout.rdbuf(),它指向一个 std::streambuf 实例(通常是 std::filebuf),而这个缓冲区在程序启动时就已初始化完毕。你不能简单地“替换”std::cout 对象本身(它是 const 的),但可以安全地替换它的缓冲区。
继承 std::streambuf 实现自定义写入逻辑
最可靠的方式是派生 std::streambuf,重写 overflow() 和 sputn() —— 这两个函数负责单字符和批量字符写入。注意:不要重写 sync() 来做日志落盘,除非你真需要 flush 语义;多数日志回调应“即写即调”,避免延迟。
常见错误现象:std::cout 只触发一次 <code>overflow()(对 \n),但 std::endl 会额外调用 sync();若你在 sync() 里又调用日志回调,会导致重复记录。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 在
overflow(int c)中处理单字符(如\n或普通字符),把字符暂存到本地 buffer,遇到\n或 buffer 满时,将整行传给你的日志回调 - 重写
sputn(const char* s, std::streamsize n)处理字符串批量写入,按行切分(查找\n)再逐行回调,避免把跨行内容拼错 - 务必在构造时调用
setp(buf_start, buf_end)初始化输出缓冲区,否则sputn可能退化为逐字符调用overflow - 缓冲区大小建议设为 1024 字节,太小导致频繁回调,太大可能丢失最后一行(无换行符时)
替换 cout 的 rdbuf 并确保线程安全
std::cout.rdbuf(new MyLogStreamBuf(log_callback)) 是可行的,但必须在所有其他线程使用 std::cout 前完成,且不能在多线程并发调用该替换操作。更稳妥的做法是在 main() 开头、甚至 std::ios_base::Init 之后立即替换。
性能影响:每次 std::cout 写入都会进入你的 streambuf,开销略高于直接 write(1, ...),但可控。如果日志回调本身是异步的(如投递到队列),别在 overflow 里加锁——应让回调函数自己处理线程安全。
兼容性注意点:
- 某些平台(如 Windows 控制台)下,替换
std::cout后printf仍走原 stdout,两者不互通;若需统一,得同时 dup2(1, …) 或用freopen,但那会绕过 C++ 流体系 -
std::cerr和std::clog是独立对象,需分别替换各自的rdbuf() - 第三方库若直接调用
write(1, ...)或fputs(..., stdout),不会经过你的streambuf
避免 std::ios_base::sync_with_stdio(false) 导致失效
如果代码中调用了 std::ios_base::sync_with_stdio(false),C++ 流会脱离 C stdio 缓冲体系,但你的 streambuf 依然有效 —— 这不是问题。真正的问题是:一旦调用 sync_with_stdio(false),后续再调用 std::cin/std::cout 就不能再混用 scanf/printf,否则行为未定义;但这不影响你重定向 cout 本身。
容易被忽略的地方:如果你的日志回调里又调用了 std::cout(比如打印调试信息),会引发无限递归。务必确保回调函数只做纯逻辑(如发消息、写文件、调用系统 write),绝不触碰任何标准流对象。


















