应使用单例std::ofstream以app模式长期打开日志文件,配合std::put_time格式化时间戳并手动提取毫秒,多线程下仅锁定文件写入段而非整条日志拼接。

用 std::ofstream 做最简日志输出,别碰全局 std::cout 重定向
重定向 std::cout 看似“一键”,实则危险:多线程下会乱序、第三方库可能意外写入、程序崩溃时缓冲区未刷盘导致日志丢失。真正可控的日志必须显式打开文件、独立管理流状态。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 每个日志写入都调用
std::ofstream的open()+write()+close()?太慢——改用单例模式持有一个长期打开的std::ofstream对象 - 构造时用
std::ios::app标志,避免每次覆盖;加std::ios::out明确用途 - 务必在析构或程序退出前调用
flush(),否则最后一段日志可能滞留在缓冲区 - 示例关键行:
log_file.open("app.log", std::ios::out | std::ios::app);
时间戳和级别字段不能靠 std::chrono 手搓,要用 std::put_time 格式化
手写 std::chrono::system_clock::now() 转字符串容易漏掉毫秒、时区错乱、格式不统一。Windows 下 localtime_s 和 Linux 下 localtime_r 接口还不兼容。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 统一用
std::put_time配合std::gmtime或std::localtime(注意线程安全) - 推荐格式:
%Y-%m-%d %H:%M:%S.+ 毫秒(需手动提取) - 毫秒提取别用除法取余——
time_point.time_since_epoch().count() % 1000更可靠 - 示例片段:
auto ms = duration_cast<milliseconds>(tp.time_since_epoch()).count() % 1000;</milliseconds>
多线程写日志必须加锁,但别锁整个 write() 过程
常见错误是把整条日志拼接 + 写入包在一个 std::lock_guard 里,导致高并发时线程排队严重,拖慢主逻辑。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 只锁文件写入那几行,日志内容拼接(含时间戳、级别、消息)提前在无锁区完成
- 用
std::mutex而非std::recursive_mutex——日志函数不该被递归调用 - 若性能敏感,可考虑无锁环形缓冲 + 单独日志线程消费,但小项目没必要
- 别用
std::ofstream::operator 多次调用——每次都是函数调用开销,拼成一个 <code>std::string再一次性write()
日志文件不能无限增长,得在类里做 file_size 检查和轮转
线上跑几天后日志占满磁盘,不是 bug 是设计缺失。C++ 标准库没提供跨平台文件大小检查,得自己封装。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用
std::filesystem::file_size()(C++17)获取当前大小,比stat()更简洁且跨平台 - 轮转策略简单有效:达到 10MB 就关旧文件、重命名(如
app.log.1),再开新app.log - 重命名前先检查
app.log.1是否存在,存在则删掉或继续编号(最多留 3 个备份) - 注意:
std::filesystem::rename()在 Windows 下对已打开文件可能失败,务必先close()再 rename
真正的“一键导出”不是靠某个魔法函数,而是把文件打开、格式化、线程安全、滚动清理这四件事,在一个类里稳稳地串起来。最容易被忽略的是:日志对象生命周期必须长于所有可能调用它的线程,否则析构时锁还在用,就崩了。










