为何time_t经localtime()与mktime()往返转换后有时无法得到原值?
问题
尝试对time_t执行往返转换:先通过struct tm *localtime(time_t t)将其转为struct tm,再通过time_t mktime(struct tm *tm)转回time_t。测试代码如下:
#include <stdbool.h> #include <stdio.h> #include <time.h> bool time_t_round_trip(time_t t) { struct tm *tm = localtime(&t); if (tm == NULL) { return false; } time_t t2 = mktime(tm); return t == t2; } #define HOUR 3600 /* when `time_t` is a count of seconds */ int main(void) { time_t now; time(&now); for (time_t t = 0; t <= now; t += HOUR) { if (!time_t_round_trip(t)) { printf("%lld %s", (long long) t, ctime(&t)); } puts("Done"); return 0; }
代码在本地时区运行正常,但切换到部分其他时区时会失败。编译环境为GNU C17 (GCC) version 12.4.0 (x86_64-pc-cygwin),依赖库版本包括GMP 6.3.0、MPFR 4.2.1、MPC 1.3.1、isl 0.26-GMP。
为测试更多时区,编写了遍历时区列表的代码:
const char *tzn[] = { "Africa/Abidjan", "Africa/Accra", ..., "Zulu", 0}; int set_tz(const char *tz) { if (tz) { if (setenv("TZ", tz, 1 /* overwrite */)) { return -1; // handle error } tzset(); printf("### tz:'%s'\n", tz); fflush(stdout); } } int main(void) { for (size_t i = 0; tzn[i]; i++) { set_tz(tzn[i]); test_times(); } puts("Done"); return 0; }
部分转换失败的时区及对应时间示例:
### tz:'Africa/Algiers' 309740400 Thu Oct 25 23:00:00 1979 ### tz:'Africa/Casablanca' 504918000 Tue Dec 31 23:00:00 1985 ### tz:'Africa/Juba' 1612126800 Sun Jan 31 23:00:00 2021 ### tz:'Africa/Khartoum' 1509483600 Tue Oct 31 23:00:00 2017 ...
请问为何该往返转换在部分时区无法得到原值?
原因分析与解决方案
核心原因:时区历史变更与struct tm信息丢失
这些转换失败的时间点,基本都对应时区的夏令时切换、标准偏移量调整这类历史事件:
localtime把time_t转成本地时间的struct tm时,会丢掉原始time_t对应的UTC偏移量信息,只保留年、月、日、时、分、秒这些本地时间字段。- 当这个本地时间在时区历史中存在歧义(一个本地时间对应多个UTC时间)或不存在的情况时,
mktime会按照当前时区规则来解析,最终生成的time_t就会和原始值不一致。
举个实际例子:假设某个时区在1979年10月26日凌晨0点把时钟拨慢1小时(取消夏令时),那么本地时间Thu Oct 25 23:00:00 1979其实对应两个不同的time_t——切换前的UTC时间和切换后的UTC时间。localtime把原始time_t转成这个本地时间后,mktime只能选其中一个(通常是切换后的那个),自然就会出现往返不匹配的问题。
另外,部分时区曾调整过标准时区偏移(比如从UTC+2改成UTC+3),这类变更也会导致特定时间点的本地时间对应多个UTC时间,或者某个本地时间在时区规则里根本不存在,进而引发转换偏差。
解决方案
要实现可靠的time_t往返转换,得避开本地时区的歧义问题,或者保留关键信息:
- 改用UTC时区转换:用
gmtime(把time_t转成UTC时间的struct tm)和timegm(把UTC的struct tm转回time_t)替代localtime和mktime。UTC没有夏令时和偏移调整的问题,转换是双向唯一的。
修改示例:bool time_t_round_trip_utc(time_t t) { struct tm *tm = gmtime(&t); if (tm == NULL) { return false; } time_t t2 = timegm(tm); // 注:timegm是GNU扩展,若需标准C可手动计算UTC偏移后用mktime return t == t2; } - 保留UTC偏移信息:如果必须用本地时区,转换时额外记录
localtime返回的struct tm的tm_gmtoff字段(该字段记录本地时间与UTC的秒差,属于GNU扩展),转回时手动调整,避免歧义。 - 使用专业时间库:比如ICU(International Components for Unicode)的时间处理模块,它能更精准地处理时区历史变更和歧义时间的解析。
内容的提问来源于stack exchange,提问作者chux
相关产品推荐
相关产品推荐

