核心是每次写日志前检查当前日期是否异于文件最后修改日期,用std::filesystem::last_write_time()与std::chrono截断到零点比对,不等则重命名旧文件并新建当日文件,滚动时顺手清理过期日志,多线程下用互斥锁保护滚动操作。

用 std::filesystem 判断日期并切分日志文件
核心不是“定时触发”,而是每次写日志前检查当前日期是否和上次写入日期不同。C++17 的 std::filesystem 能直接读取文件最后修改时间,配合 std::chrono 提取年月日即可比对。
常见错误是直接用系统时间字符串拼接文件名(如 "log_" + to_string(day) + ".txt"),但没处理跨月/跨年时旧文件残留问题——这会导致滚动不干净,后续过期逻辑失效。
- 每次打开日志文件前,先用
std::filesystem::last_write_time()读取当前日志文件的修改时间,转成std::chrono::system_clock::time_point,再用std::chrono::floor<:chrono::days>()</:chrono::days>截断到当天零点 - 同样方式处理当前系统时间,两者比较:不等就关闭旧文件、重命名(加日期后缀)、新建当日文件
- 重命名建议用
std::filesystem::rename(),它在同磁盘下是原子操作,避免写入中断导致日志丢失
用 std::filesystem::directory_iterator 扫描并删除过期日志
过期不是靠“启动时删一次”,而是每次滚动时顺手清理——这样既避免累积太多小文件拖慢扫描,又不用额外启线程或依赖外部调度。
容易踩的坑是用 std::filesystem::last_write_time() 当作“生成时间”来算过期,但 Windows 上该时间可能被编辑器或备份工具篡改;更可靠的是从文件名里解析日期(比如 "app_20240520.log")。
- 遍历日志目录时,跳过当前正在写的文件(可用文件名匹配或
is_open()判断) - 对每个候选文件,用正则或固定偏移提取日期部分,转成
std::chrono::sys_days,与当前日期做天数差计算 - 删除前先调用
std::filesystem::status()确认是普通文件,防止误删目录或符号链接
多线程写日志时如何避免滚动冲突
多个线程同时检测到要滚动,可能并发 rename 或创建同名新文件,导致覆盖或失败。不能靠“先检查再操作”的竞态逻辑。
最简方案是把滚动判断和文件操作收口到一个带锁的函数里,且锁粒度要细——只锁滚动动作本身,不锁整个日志写入流程。
- 用
std::mutex保护“检查+重命名+打开新文件”这一串操作,其他线程在此期间阻塞,但不影响已打开文件的写入 - 不要在锁内做耗时操作(比如遍历全目录删旧文件),过期清理应放在滚动完成后的解锁阶段单独异步做
- 如果使用第三方日志库(如 spdlog),确认其
daily_file_sink_mt是否已内置文件锁;自己实现时,flock()在 Linux 下更轻量,但 Windows 不支持,跨平台优先选互斥量
Windows 下 CreateFile 权限与长路径问题
在 Windows 上直接用 std::ofstream 打开带日期的长路径(如 "D:\logs\app\2024\05\app_20240520.log")容易失败,报错 "No such file or directory",其实是因为父目录不存在,或路径超 260 字符限制。
不是所有编译器默认启用长路径支持,也不是所有 std::filesystem 实现都透传了 Windows API 的权限标志。
- 写入前必须递归创建目录,用
std::filesystem::create_directories(),别依赖 ofstream 自动建目录 - 若路径可能超长,确保程序 manifest 中声明
longPathAware="true",或在代码开头调用SetCurrentDirectory(L"\\?\D:\logs")切换到可扩展路径前缀 - 避免用
std::ofstream的ios::app模式打开已有文件后再 seek,Windows 下可能因缓存导致末尾写入错位;改用ios::out | ios::ate更稳妥
事情说清了就结束。真正的难点不在“怎么切”,而在“什么时候切得干净”——日期判断不准、删除逻辑漏掉归档文件、多线程下 rename 被中断,这些细节一旦出错,日志就不可信。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











