std::ofstream 写日志在多线程下不安全,会导致数据错乱或写入失败;单线程可直接使用 std::ofstream log("app.log", std::ios::app); log

用 std::ofstream 写最简日志,但要注意线程安全
直接用 std::ofstream 往文件写字符串,是新手最快上手的方式。但它在多线程环境下会崩——不是数据错乱就是写入失败,因为 std::ofstream 本身不保证线程安全。
- 单线程场景下可直接用:
std::ofstream log("app.log", std::ios::app); log - 多线程必须加锁:用
std::mutex包裹每次写入,否则可能多个线程同时调用operator 导致缓冲区冲突 - 别忘了在每次写完调用
log.flush(),否则日志可能滞留在缓冲区里,程序崩溃时丢数据 - 打开文件时建议加
std::ios::out | std::ios::app,避免意外覆盖已有日志
为什么不用 printf 或 std::cout 直接重定向到文件
重定向 std::cout 到文件(比如 freopen("log.txt", "a", stdout))看似省事,实际隐患很多:
- 所有
std::cout输出都会进日志,包括调试输出、第三方库的打印,完全失控 -
printf系列函数在 Windows 下对中文路径/内容支持差,容易乱码或 fopen 失败 - 无法按级别过滤(比如只保留
ERROR),也没法自动加时间戳或线程 ID - 重定向后难以恢复原 stdout,尤其在 DLL 或嵌入式环境中容易引发不可预测行为
用 spdlog 做生产级日志,但得避开几个默认坑
spdlog 是目前 C++ 最常用、性能也够用的日志库,但开箱即用的配置容易踩坑:
- 默认 logger 不是异步的,高频写日志时仍会阻塞主线程;要用
spdlog::stdout_logger_mt("console")或显式创建 async logger - 文件 logger 默认不轮转,
spdlog::rotating_logger_mt("file", "app.log", 1048576 * 5, 3)才能实现 5MB×3 个文件轮转 - Windows 下若日志路径含中文,需用
spdlog::filename_t和 UTF-8 转宽字符,否则create_file_logger失败返回空指针 - 不要在全局变量里初始化 logger,尤其跨 DLL 时容易触发静态初始化顺序问题;推荐在
main()开头用spdlog::init_thread_pool()显式启动
自定义日志宏时,__FILE__ 和 __LINE__ 的实际表现
写 #define LOG_INFO(fmt, ...) spdlog::info("[{}:{}] " fmt, __FILE__, __LINE__, ##__VA_ARGS__) 很常见,但要注意:
-
__FILE__返回的是完整路径(如/home/user/proj/src/main.cpp),打日志太长;可用strrchr(__FILE__, '/')截取文件名,或用编译器扩展__BASE_FILE__ - MSVC 下
__LINE__没问题,但某些嵌入式工具链(如 IAR)不支持##__VA_ARGS__,得用变参模板替代宏 - 宏展开后如果传入带逗号的表达式(比如
LOG_INFO("x={}, y={}", a, b)),没问题;但若传入带宏的参数(如LOG_INFO("err={}", MY_MACRO)),可能因展开顺序出错 - 调试时想临时关闭日志?别用
#ifdef DEBUG包宏,而应在 runtime 用logger->set_level(spdlog::level::off),否则编译期删掉日志代码会丢失格式校验
真正麻烦的从来不是“怎么记日志”,而是“什么时候刷盘、谁来清理旧文件、崩溃时有没有最后一条日志、多进程写同一个文件会不会冲突”。这些细节没想清楚,再 fancy 的库也救不了线上问题。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











