C++ 时间戳转日期字符串:chrono、to_time_t 与 strftime
把微秒级 Unix 时间戳格式化成 YYYY-MM-DD HH:MM:SS 本地时间字符串,标准库路径是三步:std::chrono 构造 time_point,system_clock::to_time_t() 转成 time_t,再交给 C 的 strftime 输出。实现只有十几行,但有两处容易踩的细节:精度截断和 localtime 的线程安全性。
完整实现
#include <chrono>
#include <cstdint>
#include <ctime>
#include <string>
// timestamp 为 Unix 微秒时间戳
std::string timestamp_to_string(std::uint64_t timestamp)
{
auto tp = std::chrono::time_point<std::chrono::system_clock,
std::chrono::microseconds>(
std::chrono::microseconds(timestamp));
std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::tm* local = localtime(&t);
char buf[80];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local);
return std::string(buf);
} time_point 的模板参数里显式写了 microseconds 时长,这样微秒计数直接参与构造,不需要先换算成秒。
细节一:to_time_t 会丢掉秒以下精度
to_time_t 的目标是 time_t,在主流平台上是秒级整数。按 cppreference 的说法,当 time_t 精度低于 time_point 时,截断还是舍入由实现定义。也就是说微秒部分被丢弃的具体方式没有保证——需要毫秒显示时不能指望 to_time_t,应该在它之前把秒和毫秒拆开:
namespace sc = std::chrono;
auto us = sc::microseconds(timestamp);
auto secs = sc::duration_cast<sc::seconds>(us);
auto ms = sc::duration_cast<sc::milliseconds>(us - secs);
// secs.count() 走 to_time_t 得到日期时间部分,ms.count() 自行拼接 先用 duration_cast 取整秒(向零取整),余数再转毫秒,两个部分各自确定,不再依赖实现的舍入行为。
细节二:localtime 非线程安全
localtime 返回的指针指向一个静态分配的 struct tm,下一次调用(任何线程的)就会覆盖它。Linux 手册在线程安全性一节明确标注它是 MT-Unsafe。单线程工具代码里用没有问题;多线程环境下应改用调用方提供缓冲区的版本:
std::time_t t = /* ... */;
std::tm tm_buf{};
localtime_r(&t, &tm_buf); // POSIX
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf); POSIX 提供的是 localtime_r;Windows/MSVC 一侧对应 localtime_s。这两个可重入版本以及 C 标准附录 K 的 localtime_s 之间参数顺序并不一致,使用时以目标平台的文档为准。
C++20 的替代路径
C++20 起,std::format 支持 chrono 类型与格式说明符,配合 std::chrono::zoned_time 可以直接按本地时区输出,不再需要手工走 time_t。不过这条路径依赖较新的编译器和平台的时区数据库支持,跨平台、对工具链版本没有控制的项目里,chrono + to_time_t + strftime 仍是稳妥做法。
结语
- 三步转换:
time_point<microseconds>构造 →to_time_t→strftime; to_time_t的秒以下精度截断/舍入是实现定义的,需要毫秒时先duration_cast拆分;localtime返回静态缓冲区、非线程安全,多线程用localtime_r/localtime_s(注意两者参数顺序不同)。