日志轮转需手动在每次写入前检查文件大小,用stat()跨平台获取尺寸,倒序重命名旧文件(如.log→.1、.1→.2),并用互斥锁+原子变量保证多线程安全,Windows下须先close再rename。

用 std::ofstream 检查文件大小并触发轮转
核心逻辑不是靠库自动做,而是每次写日志前手动判断当前日志文件是否超限。C++ 标准库没内置滚动功能,得自己读 std::filesystem::file_size()(C++17 起)或用 stat()(跨 C++ 版本更稳)。
常见错误是只在程序启动时检查一次大小,结果日志越写越大;正确做法是在每次调用 log() 函数写入前检查。
- 推荐用
stat():兼容性好,Windows 用_stat(),Linux/macOS 用stat(),封装一层就行 - 阈值别设太小(如 1KB),频繁打开关闭文件影响性能;也别太大(如 1GB),排查问题时难加载
- 注意文件句柄未关闭导致
stat()返回旧大小——写完必须flush(),且轮转后要确保旧std::ofstream对象已析构或close()
轮转时怎么重命名旧文件(access.log → access.log.1)
不能直接用 std::filesystem::rename() 简单覆盖,因为已有 .1 时,再轮转要变成 .2,还得把原来的 .1 变成 .2,依此类推——本质是个倒序移动过程。
容易踩的坑是循环重命名时顺序错了,比如先重命名 .1 → .2,再 current → .1,结果 .1 被覆盖丢失。
立即学习“C++免费学习笔记(深入)”;
- 从最大编号开始倒着移:先
access.log.9 → access.log.10,再.8 → .9……最后access.log → access.log.1 - 最大保留份数建议设为 5~10,太多磁盘占满,太少查不到历史
- Windows 下对正在写的文件调用
rename()会失败(权限拒绝),需确保std::ofstream已close()
多线程写日志时如何避免轮转冲突
多个线程同时判断“该不该轮转”,可能一起执行重命名+清空,导致部分日志丢失或文件损坏。这不是加个 std::mutex 就能全解决的——锁粒度太粗会卡住所有日志写入。
真正关键的是把“检查+轮转”这个动作做成原子操作,并让其他线程感知到轮转已完成。
- 用一个全局
std::mutex保护整个轮转流程(检查、重命名、reopen),但只在真正需要轮转时才上锁,平时写日志不锁 - 轮转完成后,更新一个原子变量
std::atomic<bool> rotating{false}</bool>,避免其他线程重复触发 - 不要在轮转过程中长期持有日志文件句柄;
close()后立即open()新文件,中间空隙越短越好
Windows 下 CreateFile 和 fopen 的权限差异影响轮转
用 fopen("access.log", "a") 在 Windows 上默认以共享模式打开,其他进程(比如记事本)能同时读;但轮转时用 rename() 会失败,因为 fopen 打开的句柄没释放独占锁。
而 CreateFile 可显式控制 dwShareMode,但 C++ 日志库一般不直接用它——所以更实际的解法是绕过。
- 轮转前务必调用
fflush()+fclose(),确保 C 运行时缓冲区清空、句柄释放 - 如果用了
std::ofstream,记得调用flush()后再close(),别依赖析构——析构时机不确定 - 实在不行,轮转时用
MoveFileEx配合MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,比rename()更鲁棒
最麻烦的其实是信号安全和进程意外退出:没来得及 close 文件就崩溃,下次启动时得校验当前日志是否损坏、是否需要强制轮转。这部分没法靠标准库兜底,得结合 atexit() 或 RAII 守护对象来做,但依然有死角。



















