判断是否跨天滚动日志需每次写入前用std::filesystem::last_write_time()获取文件修改时间,统一转为sys_days后与当日起始时刻比较;滚动时须fclose再fopen新文件,否则日志仍写入旧归档文件。

如何判断是否需要新建日志文件(按天)
核心是每次写日志前检查当前文件的修改时间是否跨天。不要依赖程序启动时间或缓存的日期字符串,否则重启后可能漏滚。用 stat() 或 std::filesystem::last_write_time()(C++17)读取文件最后修改时间,再转为本地日时钟的年月日做比对。
常见错误:直接用 localtime(&time(nullptr)) 拿当天日期,和文件时间比——但文件时间是 UTC 还是本地时?不统一就会错判。务必都转成同一时区的 tm 结构,或统一用 std::chrono::floor<std::chrono::days>() 截断到天精度比较。
- 推荐做法:每次打开文件写入前,调用一次
get_today_as_days()获取今天起始时刻(sys_days),再用std::filesystem::last_write_time(path)读文件时间,转成sys_time后 floor 到天,两者不等就滚动 - 注意 Windows 下
last_write_time()返回的是 UTC 时间,而system_clock::now()是 UTC,可直接比;Linux 也一致,无需手动时区转换 - 如果不用 C++17 文件系统,用
stat()+st_mtime,再用gmtime()或localtime()解析时注意时区一致性
滚动时如何安全重命名旧文件
滚动本质是把当前日志文件改名为带日期后缀的归档文件,比如 app.log → app.log.2024-06-15。关键在于原子性和并发安全:不能让写线程在 rename 过程中往原文件写入,也不能让 rename 失败后丢失日志。
常见错误:先 close 再 rename 再 reopen——中间有空隙,且多进程下可能被其他实例覆盖。正确做法是保持文件描述符打开,仅对路径做操作,并用 fsync() 确保内容落盘后再 rename。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- 单线程场景:写前加锁 →
fflush()+fsync(fd)→rename(old_path.c_str(), new_path.c_str())→ 清空并 truncate 原文件(或直接 fopen(..., "w"))→ 解锁 - 多进程/多线程必须用文件锁(
flock())或命名互斥量,否则两个进程同时判断“要滚动”,会冲突 rename 或清空 - Windows 上
MoveFileEx(..., MOVEFILE_REPLACE_EXISTING)更健壮,Linux 用renameat2()(需内核 3.15+)支持原子替换
如何控制备份文件数量(避免磁盘打满)
只滚动不清理,几个月后几百个 .log.YYYY-MM-DD 文件会占满磁盘。清理动作不能在每次写日志时遍历所有文件(性能差),应设为滚动时的附带操作,且只查匹配 pattern 的文件。
常见错误:用 std::filesystem::directory_iterator 遍历整个目录,未过滤就删——可能误删配置文件或其它日志。必须严格匹配命名规则,比如正则 ^app\.log\.\d{4}-\d{2}-\d{2}$。
- 建议滚动成功后,调用清理函数:glob
app.log.*→ 提取日期 → 排序 → 删除早于N天的(如保留 30 天) - 避免用
std::regex在循环里匹配(C++11 regex 性能差),改用substr()+isdigit()快速校验后 10 字符是否形如2024-01-01 - 清理失败(如权限不足)不应中断主日志流程,记一条 warning 即可,否则可能导致日志完全停摆
为什么 fopen("app.log", "a") 不足以支撑滚动
很多人以为用追加模式打开文件,滚动时只要 rename 就行——但 FILE* 内部缓冲区仍指向原 inode,rename 后继续 fwrite() 会写进已重命名的旧文件,而不是新创建的 app.log。这是最隐蔽也最常踩的坑。
根本原因:fopen("app.log", "a") 打开的是路径名,不是文件本身;rename 只改路径映射,不改变已打开的 fd 指向。必须显式 fclose() 当前句柄,再 fopen("app.log", "a") 重新打开新文件。
- 务必在 rename 后执行
fclose(m_log_file)→m_log_file = fopen("app.log", "a"),否则日志会持续写进归档文件 - 使用
std::ofstream同理:需m_ofstream.close()后再m_ofstream.open("app.log", std::ios::app) - 若用
dup2()重定向 stdout/stderr 到日志文件,滚动时需close(1)+open("app.log", O_APPEND|O_WRONLY)+dup2(),否则子进程输出仍写旧文件
跨天滚动看着简单,真正稳定运行要同时处理好时间判定精度、文件重命名原子性、多进程竞争、句柄刷新时机、以及清理策略的边界条件——尤其是 fopen 后 rename 不自动切换目标这个点,90% 的自研日志模块初期都栽在这儿。


















