推荐用 std::chrono::system_clock::now() 获取高精度时间,结合 localtime_s(windows)或 localtime_r(linux)转本地时区,毫秒通过 time_since_epoch().count() % 1000000 计算;日志接口采用可变模板参数封装,避免 va_list,优先用 std::ostringstream 或 c++20 std::format。

直接用 std::chrono + std::ostringstream 就能做出线程安全、带毫秒级时间戳、可重定向输出的轻量日志类,不用第三方库,也不必依赖 strftime 这类 C 风格函数。
如何生成高精度本地时间戳(含毫秒)
Windows 和 Linux 下都推荐用 std::chrono::system_clock 获取当前时间,再转为本地时区并格式化。别用 std::time(nullptr),它只到秒级,且不处理时区。
- 用
std::chrono::system_clock::now()取时间点,转成std::time_t得到秒数,再用std::localtime转本地结构体 - 毫秒部分要单独算:
time_point.time_since_epoch().count() % 1000(单位是纳秒时除 1000000) - 注意:
std::localtime在多线程下非线程安全,应改用std::localtime_r(Linux)或localtime_s(MSVC)
如何封装可变参数的日志接口(类似 printf)
C++11 起用可变模板参数比宏更安全,也支持类型检查。不要用 va_list 手动解析,容易出错且无法自动推导类型。
- 定义模板函数
log(const char* fmt, Args&&... args),内部用std::sprintf或std::format(C++20)拼接 - 若不用 C++20,推荐用
std::ostringstream+流式拼接,避免格式符错误(比如传 <code>std::string却写%s) - 宏包装(如
LOG_INFO)仍可保留,但只做__FILE__、__LINE__和级别注入,实际输出走模板函数
如何保证多线程下日志不乱序、不崩溃
最简单的办法是每个日志调用都加 std::mutex 锁,但性能差;更合理的是把日志暂存到线程局部缓冲,再由单个日志线程统一刷盘。
立即学习“C++免费学习笔记(深入)”;
- 轻量场景:在
log()入口加static std::mutex mtx,锁住整个格式化+输出流程 - 注意:锁的范围不能只包
std::cout,必须包含时间戳生成和字符串拼接,否则时间/内容可能错位 - 若用
std::ofstream写文件,记得调用flush(),否则缓冲区内容可能滞留 - 避免在析构函数里写日志——对象销毁顺序不确定,
std::ofstream可能已关闭
如何支持日志级别和运行时开关
用一个全局 enum class LogLevel 和静态变量控制最低输出级别,比预处理器宏更灵活,支持运行时调整。
- 定义
enum class LogLevel { DEBUG, INFO, WARNING, ERROR },成员值对应整数便于比较 - 设静态变量
static LogLevel g_min_level = LogLevel::INFO,每次 log 前先判断level >= g_min_level - 级别字符串建议用
constexpr std::array查表,而不是一堆if-else,避免分支预测失败 - 如果需要动态修改,提供
set_log_level(LogLevel)接口,无需重启程序
真正难的不是打日志,而是确保时间戳和消息严格对应、多线程下不丢日志、以及关闭日志时不意外触发析构顺序问题——这些细节不显眼,但线上出问题时最难排查。










