多线程下直接用std::ofstream写日志会因无锁、无缓冲导致句柄竞争、乱序和行截断;须复用对象、append模式、手动flush,并用do{...}while(0)宏保证安全展开。

为什么直接用 std::ofstream 写日志会出问题
直接在多线程场景下反复 open() / close() 同一个 std::ofstream,大概率触发文件句柄竞争或写入乱序。更隐蔽的是,std::ofstream 默认不加锁,也不缓冲对齐,日志行可能被截断(比如两线程同时写 "INFO: a
" 和 "WARN: b
",结果变成 "INFO: a
W" + "ARN: b
")。必须显式控制打开模式、缓冲策略和同步时机。
-
std::ofstream构造时传std::ios::app,避免覆盖;但不要每次写都open(),应复用对象 - 调用
file.rdbuf()->pubsync()或file.flush()强制落盘——尤其在程序异常退出前 - 若需线程安全,不能只靠
std::mutex锁整个写操作,而应在operator<<流插入前完成格式化,减少临界区长度
LOG_INFO 这类宏里为什么要用 do { ... } while(0)
宏展开后要能安全出现在 if 语句分支、循环体等任意上下文中,否则会因分号或大括号缺失导致编译错误或逻辑错位。比如裸写成 #define LOG_INFO(x) std::cout << x << "
",遇到 if (flag) LOG_INFO("a"); else LOG_INFO("b"); 就会因宏展开无大括号而绑定错 else。
- 必须包裹为
do { /*...*/ } while(0),这样宏调用末尾的分号才真正终结语句 - 宏体内变量要用唯一前缀(如
_log_line_)避免与用户代码重名冲突 - 别在宏里直接调用
std::endl——它强制 flush,性能差;用" "配合手动 flush 更可控
如何让日志自动带文件名、行号和时间戳
预处理器宏 __FILE__ 和 __LINE__ 是编译期常量,可直接拼进日志头;但 std::chrono::system_clock::now() 必须运行时获取,不能塞进宏定义体。正确做法是把时间戳生成逻辑下沉到实际写入函数里,宏只负责传递位置信息。
- 宏定义中用
__FILE__提取文件名:用strrchr(__FILE__, '/')或strrchr(__FILE__, '\')找最后斜杠,再 +1 得干净文件名 - 时间戳建议用
std::put_time(&tm, "%Y-%m-%d %H:%M:%S")格式化,避免std::to_string拼接年月日带来的性能开销 - 注意
localtime_r(Linux)或localtime_s(Windows)线程安全,别用localtime
为什么日志库初始化失败后,宏仍可能尝试写文件
宏展开是纯文本替换,不感知运行时状态。如果 LogManager::instance().init("app.log") 失败了,后续所有 LOG_INFO 宏仍会调用底层写函数——而该函数若没判空就直接往未打开的 std::ofstream 写,会导致 failbit 置位且无输出,还可能引发未定义行为。
立即学习“C++免费学习笔记(深入)”;
- 写函数入口必须检查
file.is_open(),未打开则退化为std::cerr输出或直接丢弃 - 宏本身不做初始化判断,那是使用者的责任;但底层实现必须有兜底,不能假设“调用了宏就一定 init 过”
- 建议在
LogManager析构时自动 flush 并 close,避免进程退出时缓冲区残留
最易被忽略的一点:日志文件路径若含中文或空格,std::ofstream 在 Windows 下可能因编码问题打不开——得用 std::filesystem::u8path()(C++17)转 UTF-8 路径,或改用宽字符接口 std::wofstream 配合 L"xxx.log"。


















