C语言中 time_t 与 struct tm 的互转 是时间处理中最常见且最容易出错的部分之一。
下面从原理、常见写法、坑点、高效/安全实现方式、跨平台注意事项等角度完整梳理,帮助你彻底搞懂并写出可靠的代码。
核心概念(先搞清楚这几个才不会写错)
| 类型/函数 | 含义 | 时区相关性 | 是否可修改 | 典型用途 |
|---|---|---|---|---|
time_t | 从 1970-01-01 00:00:00 UTC 到现在的秒数(通常是 signed 64位或 32位) | 无(UTC) | — | 存储、传输、比较时间戳 |
struct tm | 人类可读的分解时间(年月日时分秒、星期、是否夏令时等) | 有(本地时区或指定时区) | 可修改 | 显示、输入、格式化 |
time() | 获取当前 UTC 时间戳 | 无 | — | 获取当前 time_t |
gmtime() | time_t → struct tm(UTC) | 无 | 返回静态缓冲区 | 获取 UTC 分解时间 |
localtime() | time_t → struct tm(本地时区) | 有 | 返回静态缓冲区 | 获取本地分解时间 |
mktime() | struct tm → time_t(按本地时区解释) | 有 | 修改输入的 tm_wday/tm_yday | 把分解时间转为时间戳 |
timegm() | struct tm → time_t(按 UTC 解释) | 无 | 修改输入的 tm_wday/tm_yday | 非 POSIX,但很多系统支持 |
最关键的一句话:
gmtime / localtime:time_t → struct tmmktime:struct tm → time_t(按本地时区规则解释)timegm:struct tm → time_t(强制按 UTC 解释)
常见需求 & 推荐写法(2025–2026 视角)
1. time_t → struct tm(最常用两种场景)
#include <time.h>
#include <stdio.h>
void print_time_examples(time_t t) {
// 方式1:转为 UTC 时间
struct tm utc_tm;
gmtime_r(&t, &utc_tm); // 线程安全推荐写法
printf("UTC: %04d-%02d-%02d %02d:%02d:%02d\n",
utc_tm.tm_year + 1900, utc_tm.tm_mon + 1, utc_tm.tm_mday,
utc_tm.tm_hour, utc_tm.tm_min, utc_tm.tm_sec);
// 方式2:转为本地时间
struct tm local_tm;
localtime_r(&t, &local_tm); // 线程安全
printf("Local: %04d-%02d-%02d %02d:%02d:%02d %s\n",
local_tm.tm_year + 1900, local_tm.tm_mon + 1, local_tm.tm_mday,
local_tm.tm_hour, local_tm.tm_min, local_tm.tm_sec,
local_tm.tm_isdst > 0 ? " (夏令时)" : "");
}
重要:永远不要用 gmtime() / localtime() 这种非线程安全的版本(它们返回同一个静态缓冲区,多线程极易出问题)。
现代代码一律用 _r 后缀版本:gmtime_r / localtime_r
2. struct tm → time_t(最容易出错的部分)
// 场景1:已知 struct tm 是本地时间,想得到对应的 time_t
time_t local_tm_to_time_t(struct tm *tm) {
tm->tm_isdst = -1; // 让 mktime 自己判断是否夏令时(最安全)
return mktime(tm);
}
// 场景2:已知 struct tm 是 UTC 时间,想得到 time_t(UTC 秒数)
time_t utc_tm_to_time_t(struct tm *tm) {
#ifdef _WIN32
// Windows 没有 timegm,使用下面替代写法
return _mkgmtime(tm);
#else
// Linux / macOS / BSD 大多支持
return timegm(tm);
#endif
}
Windows 没有 timegm(),但提供了 _mkgmtime()(效果相同)。
3. 跨平台统一写法(推荐生产使用)
// 跨平台 timegm 实现
time_t portable_timegm(struct tm *tm) {
// 保存原始值
int y = tm->tm_year;
int m = tm->tm_mon;
int d = tm->tm_mday;
int h = tm->tm_hour;
int min = tm->tm_min;
int s = tm->tm_sec;
#ifdef _WIN32
return _mkgmtime(tm);
#else
return timegm(tm);
#endif
// 如果平台不支持 timegm,可以用下面这种可靠但稍慢的方式
// time_t t = mktime(tm);
// if (t == (time_t)-1) return (time_t)-1;
// struct tm gm;
// gmtime_r(&t, &gm);
// long offset = (long)(mktime(&gm) - t);
// return t - offset;
}
常见坑 & 雷区(一定要记住)
- 不要忘记 +1900 / +1
tm_year是从 1900 年开始的偏移量tm_mon是 0~11(不是 1~12)
- tm_isdst 的正确用法
-1:让系统自动判断(推荐)0:明确不是夏令时>0:明确是夏令时
- mktime 会修改输入的 struct tm
- 会规范化字段(比如把 32 号自动变成下个月 1 号)
- 会填充
tm_wday和tm_yday
- 32 位 time_t 的 2038 年问题
- 32 位 signed time_t 在 2038-01-19 03:14:07 UTC 溢出
- 现代系统基本都用 64 位 time_t(Linux 内核从 5.6 开始默认 64 位)
- 多线程安全
- 永远用
_r版本 - 不要在信号处理函数中调用这些函数(非异步信号安全)
总结:2025–2026 年推荐的“标准”互转模板
#include <time.h>
// time_t → UTC struct tm(线程安全)
void get_utc_tm(time_t t, struct tm *out) {
gmtime_r(&t, out);
}
// time_t → 本地 struct tm(线程安全)
void get_local_tm(time_t t, struct tm *out) {
localtime_r(&t, out);
}
// 本地 struct tm → time_t
time_t local_tm_to_timestamp(struct tm *tm) {
tm->tm_isdst = -1;
return mktime(tm);
}
// UTC struct tm → time_t(跨平台)
time_t utc_tm_to_timestamp(struct tm *tm) {
#ifdef _WIN32
return _mkgmtime(tm);
#else
return timegm(tm);
#endif
}
如果你当前项目有以下需求之一,可以告诉我,我可以给出更针对性的代码:
- 需要处理特定时区(非本地 / 非 UTC)
- 需要格式化输出(strftime)
- 需要解析字符串转 time_t(strptime + mktime)
- 涉及数据库存储的时间戳(MySQL、PostgreSQL、Redis 等)
- 需要兼容 32 位系统或嵌入式环境
祝你写出没有“时间坑”的代码!