C++ 日期计算接口:mktime、gmtime、localtime与boost::posix_time
过去的一个星期日(11月1号),美国的冬令时回归,使用了夏令时的州人民都需要在这一天将时钟往前拨一个小时,现代智能设备基本都支持自动的时间跳变,11月1号凌晨2点一到,全美的时钟会再次自动跳转至1点;如果你忘记为机械表调整时间,那么明天9点你到学校,估计只有若干个和你一样忘记调冬令时的伙伴。对夏令时而言,则是在春天前(一般是3月的第二个周日2点),往后调一个小时至三点。
目前全球目前只有少部分国家使用夏令时制度,但在上世纪尤其是战时夏令时非常普遍,中国在1986年也尝试实行夏令时制度,1991年废止,夏令时旨在最大程度利用白天光照,起到节能增效目的,本质就是春夏天日照时间长时早起一小时、早睡一小时,照明的时间也相应减少,电费也少交了。但进入21世纪这样的节能制度没什么效果,因为空调成本可能还更高,这也是为什么许多国家正意欲废除夏令时制度。
夏令时也是影响本地时间的因素之一,因此程序设计中了解本地时间和UTC时间很有必要,C/C++也提供许多跨平台接口。
时间戳转具体日期时间
计算机系统定义1970-01-01 00:00:00 UTC起,Unix时间戳指的就是从这个时间刻过去的秒数,常常被定义为time_t或者int64_t类型,但可读性很低,因此一般需要转换成常见日期文本打印。
C++提供了gmtime和localtime函数来进行转换,但是这二者本质返回的struct tm*是静态缓冲区中共享的指针,不具备线程安全,因此现代C++基本废弃,转而使用localtime_s/localtime_r和gmtime_s/gmtime_r,由调用者提供时间缓冲区,具备线性安全。见下文。
struct tm/std::tm
tm是C语言<time.h>描述时间的结构体,后续也直接进入了C++标准库<ctime>,两种定义是完全等效的:
1
2
3
4
5
6
7
8
9
10
11struct tm {
int tm_sec; //0-60, 注意,60s是闰秒
int tm_min; //0-59
int tm_hour; //0-23
int tm_mday; //1-31
int tm_mon; //0-11,注意,月份-1
int tm_year; //1900起的年份
int tm_wday; //星期,0-6,0是周日
int tm_yday; //0-365,一年中第几天
int tm_isdst; //夏令时标志,对gmtime恒为0,对localtime取决于时区,也可进行自定义
};
闰秒
题外话,特别注意的是,23:59:60是国际计量大会在1972年决定引入的闰秒。世界时钟有两个度量标准:
世界时(UT):世界时是基于地球自转计算的时间,但长远来看,地球自转变慢,一天的时长会逐渐增长;
原子时(TAI):原子时是人类衡量1秒最精确的系统手段,大概是定义了铯原子在某两个精细能级跃迁发出的辐射持续n多个周期,就是精确的1s,这套系统非常稳定,是所有精细时间网络使用的计时底层;
可见,二者一定会存在偏差,于是国际计量单位在1972年提出了协调世界时方案,这就是目前时钟最常用的UTC,约定:当二者相差为0.9s时,会人为增加1s,时钟会走到60s,即闰秒。
上述几行话只有历史和认知价值,没有任何程序设计价值,因为:
闰秒的出现无周期、不可长期预料,1972至今一共出现了27次,最近一次在2016年,出现前半年国际计量组织会发公告让全世界时钟单位提前处理;
今年国际计量大会决议通过,2027年5月20起,闰秒会正式废止;
因为现代交易系统、机场等因为临时插入闰秒造成过不少事故,因此没有比有还好,允许UT和TAI偏差3600s(这可能要花费数千年)。
gmtime_s/gmtime_r
gmtime用于获取不带时区的UTC时间,其中gmtime_s用于windows平台,gmtime_r用于linux平台,注意,二者的参数是相反的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18auto timestamp2date_utc = [](time_t timestamp){
cout << "orignal: " << timestamp << endl;
std::tm res{};
gmtime_s(&res, ×tamp);
gmtime_r(×tamp, &res);
//put_time格式化打印:
stringstream oss;
oss << std::put_time(&res, "%Y-%m-%d %H:%M:%S %Z\n");
cout << oss.str() << endl;
};
auto now = std::chrono::system_clock::now();
time_t tnow = std::chrono::system_clock::to_time_t(now);
timestamp2date_utc(tnow); //2026-11-03 08:15:41 UTC(本地时间为UTC+0800,16:15:41)
localtime_s/localtime_r
localtime则是用于从时间戳获取本地时间,会自动加上当前系统的时区偏差:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18auto timestamp2date_local = [](time_t timestamp){
cout << "orignal: " << timestamp << endl;
std::tm res{};
localtime_s(&res, ×tamp);
localtime_r(×tamp, &res);
//put_time格式化打印:
stringstream oss;
oss << std::put_time(&res, "%Y-%m-%d %H:%M:%S %Z\n");
cout << oss.str() << endl;
};
auto now = std::chrono::system_clock::now();
time_t tnow = std::chrono::system_clock::to_time_t(now);
timestamp2date_local(tnow); //2026-11-03 16:15:41
Linux查看和设置时区
linux可以使用timedatectl指令查看时区:
1
2
3
4
5
6
7
8
9
10timedatectl
# 输出:
# Local time: 二 2026-11-03 16:24:29 CST
# Universal time: 二 2026-11-03 08:24:29 UTC
# RTC time: 二 2026-11-03 08:24:28
# Time zone: Asia/Shanghai (CST, +0800)
# System clock synchronized: no
# NTP service: inactive
# RTC in local TZ: no
或者date: 1
2
3
4
5date
# 2026年 11月 03日 星期二 16:24:52 CST #后三位带时区
date +%z #时区偏差
# +0800
设置时区: 1
2
3
4
5
6# 列出所有可用时区
timedatectl list-timezones | grep Shanghai
#Asia/Shanghai
# 设置时区
sudo timedatectl set-timezone Asia/Shanghai
C++查看和设置时区
cpp可以通过setenv和tzset影响进程的时区(不影响系统),用于编写测试用例,但注意,这两个函数修改的是进程的全局状态,在多线程环境是不安全的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using namespace std;
int main(int, char**){
auto timestamp2date = [](time_t timestamp){
cout << "orignal: " << timestamp << endl;
std::tm res{};
localtime_s(&res, ×tamp);
localtime_r(×tamp, &res);
cout << "Use summer time: " << res.tm_isdst << endl; //在11月1号前,会返回1
//put_time格式化打印:
stringstream oss;
oss << std::put_time(&res, "%Y-%m-%d %H:%M:%S %Z\n");
cout << oss.str() << endl;
};
auto now = std::chrono::system_clock::now();
time_t tnow = std::chrono::system_clock::to_time_t(now);
//设置美东时区
_putenv_s("TZ", "EST5EDT,M3.2.0,M11.1.0"); // MSVC
_tzset(); // MSVC
setenv("TZ", "America/New_York", 1); // POSIX
tzset(); // POSIX
timestamp2date(tnow);
}
具体日期时间转时间戳_mkgmtime/timegm/std::mktime
根据日期和具体时间,C++可以转换成对应的unix时间戳或者本地时间戳,其中:
_mkgmtime: windows系统,把参数直接当UTC时间,直接转换为对应的unix时间戳;timegm:linux系统,同上,把参数直接当UTC时间,直接转换为对应的unix时间戳;std::mktime:跨平台,把参数当本地日期,转unix时间戳前会将程序时间减去系统时区偏差信息。
以下:
1 | auto timestamp2date_utc = [](time_t timestamp){ |
C++ 20方案:时间戳和日期的互转
C++
20的std::chrono能统一处理时间戳和日期的转换,支持跨平台且通过std::format打印,以下。
时间戳转具体日期
在C++
20中,std::chrono::time_point<std::chrono::system_clock>本身就是utc时间戳,支持time_t直接构造:
1
2
3
4
5
6
7
8
9
10
11auto timestamp2date = [](time_t timestamp){
cout << "orignal: " << timestamp << endl;
//转UTC时间戳:time_point自身就是UTC时间,无需gmtime转换
std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds> utcTime(std::chrono::seconds{timestamp});
std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", utcTime) << "\n";
//转本地时间戳(可能失效,建议测试后使用,或者乖乖用localtime):
std::chrono::zoned_time zt{std::chrono::current_zone(), utcTime};
std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", zt) << "\n";
};std::chrono::current_zone()由编译器进行实现,可能退化成UTC时间,不太可靠,建议还是沿用此前的localtime_x函数。
具体日期转时间戳
std::chrono::time_point能够直接将对应的时间转换至unix时间戳,也能根据current_zone()转换(此时类似std::mktime);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37auto timestamp2date_utc = [](time_t timestamp){
cout << "orignal: " << timestamp << endl;
//转UTC时间戳:time_point自身就是UTC时间,无需gmtime转换
std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds> utcTime(std::chrono::seconds{timestamp});
std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", utcTime) << "\n";
};
auto date2Timestamp = [×tamp2date_utc](int year, int month, int day, int hour, int min, int sec){
auto ymd = std::chrono::year_month_day{
std::chrono::year{year},
std::chrono::month{static_cast<unsigned>(month)},
std::chrono::day{static_cast<unsigned>(day)}
};
//绑定到UTC时间戳并且打印
std::chrono::time_point<std::chrono::system_clock ,std::chrono::seconds> st
= std::chrono::time_point<std::chrono::system_clock ,std::chrono::days>{ymd} + std::chrono::hours{hour} + std::chrono::minutes{min} + std::chrono::seconds{sec};
time_t utc_time = std::chrono::system_clock::to_time_t(st);
//orignal: 1793404800
//2026-10-31 00:00:00
timestamp2date_utc(utc_time);
//绑定到本地时间并且转换(可能失效,建议测试后使用,或者乖乖用std::mktime)
std::chrono::local_time<std::chrono::seconds> ltime
= std::chrono::local_days{ymd} + std::chrono::hours{hour} + std::chrono::minutes{min} + std::chrono::seconds{sec};
std::chrono::zoned_time zt{std::chrono::current_zone(), ltime};
time_t local_time = std::chrono::system_clock::to_time_t(zt.get_sys_time());
//orignal: 1793376000
//2026-10-30 16:00:00
timestamp2date_utc(local_time);
};
date2Timestamp(2026, 10, 31, 0, 0 ,0); //2026-10-31 00:00:00
检查std::chrono::current_zone()的可用性
如上述,平台编译器版本和环境不同,std::chrono::current_zone()很可能退化成utc,可能也要经过一系列设置以让其获取时区,通过以下代码能进行环境初步设置:
1
2
3
4
5
6std::chrono::zoned_time zt{std::chrono::current_zone(), ltime};
auto* zone = std::chrono::current_zone();
auto info = zone->get_info(zt.get_sys_time());
std::cout << "zone=" << zone->name()
<< ", abbrev=" << info.abbrev
<< ", offset=" << info.offset.count() << " seconds\n";
示例:判断时间是否在某天重叠
日期判断中,经常使用的是判断某个时间戳是否落在某个目标时间区间,但也有很多坑:
不可能直接判断字符串:部分月份是31天,还要处理闰秒;
不可以直接按
time_t是否相差24*60*60,因为部分时区存在夏令时干扰。
既要根据time_t判断,又要考虑本地时区,那么方案就是指向std::mktime了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25static bool LocateInDate(std::time_t timestamp_sec, std::time_t v)
{
std::tm day{};
localtime_s(&day, ×tamp_sec);
localtime_r(×tamp_sec, &day);
// 清零 → 本地当天 00:00:00
day.tm_hour = 0;
day.tm_min = 0;
day.tm_sec = 0;
day.tm_isdst = -1; //让 mktime 重新判定夏令时
std::time_t start = std::mktime(&day);
if (start == (std::time_t)-1) return false;
std::tm next = day;
next.tm_mday += 1;
next.tm_isdst = -1;
std::time_t end = std::mktime(&next);
return v >= start && v < end;
}
boost::posix_time方案:时间戳和日期的互转
上述接口基本能帮你完成大部分的业务判断,但似乎还没有很好的方案,例如一些很蛋疼的问题:
编译器的C++ 20支持不佳,无法大规模使用std::chrono的时区特性;
std::mktime支持本地时间转UTC,支持直接转的_mkgmtime/timegm甚至没进入标准库;gmtime/localtime不能跨平台,天然需要宏区分两套平台软件;std::tm结构体表示太原始,部分接口存在线程安全问题导致数据纂改;一般
time_t为秒级精度,对毫秒级支持不佳,而且没有区分时间点和时间间隔,和int类型可混合运算;
这就是为什么C++
20前boost库被广为称颂的原因,boost库通过boost.date_time提供了丰富的日期计算功能。
boost::posix::ptime
不同于std::tm,boost库使用boost::posix::ptime来表示时间点信息,这个时间点信息不包括时区信息,代表哪个时区需要提前约定,可支持毫秒级精度,存在若干种构造方法,若日期参数非法会抛出异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14//相关头文件
namespace bpt = boost::posix_time;
namespace bgg = boost::gregorian;
bpt::ptime p1(bgg::date(2026, 10,31), bpt::time_duration(1,2,3));
cout << p1 << endl; //2026-Oct-31 01:02:03
bpt::ptime p2(bgg::date(2026, 10,31), bpt::hours(0) + bpt::minutes(59) + bpt::seconds(59) + bpt::milliseconds(250));
cout << p2 << endl; //2026-Oct-31 00:59:59.250000
也可以从字符串解析,字符串格式要么是常见日期格式,或者是ISO格式:
1
2
3YYYY-MM-DD HH:MM:SS.微秒 #常见日期
YYYYMMDDTHHMMSS.微妙 #ISO格式1
YYYY-MM-DDTHH:MM:SS.微秒 #ISO格式21
2
3
4
5
6
7
8
9
10namespace bpt = boost::posix_time;
bpt::ptime p1 = bpt::time_from_string("2026-10-31 00:00:01.250");
cout << p1 << endl; //2026-Oct-31 00:00:01.250000
bpt::ptime p2 = bpt::from_iso_string("20261031T010201.25");
cout << p2 << endl; //2026-Oct-31 01:02:01.250000
bpt::ptime p3 = bpt::from_iso_extended_string("2026-10-31T01:30:01.250");
cout << p3 << endl; //2026-Oct-31 01:30:01.250000
如果使用字符串,需要拼接字符串,可以使用字符串流的方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15auto getStr = [](int year, int month, int day, int hour, int min, int sec, int millsec){
stringstream ss;
ss << std::setfill('0')
<< std::setw(4) << year << '-'
<< std::setw(2) << month << '-'
<< std::setw(2) << day << ' '
<< std::setw(2) << hour << ':'
<< std::setw(2) << min << ':'
<< std::setw(2) << sec << '.'
<< std::setw(3) << millsec;
return ss.str();
};
bpt::ptime p4 = bpt::time_from_string(getStr(2026, 10,31, 0, 1, 2, 134));
cout << p4 << endl; //2026-Oct-31 00:01:02.134000
或者从time_t解析: 1
2
3
4
5namespace bpt = boost::posix_time;
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now); //注意,这是UTC时间,无时区
bpt::ptime p1 = bpt::from_time_t(time);
cout << p1 << endl; //2026-Nov-04 08:22:58(本地为2026-Nov-04 16:22:58)
boost::posix_time::second_clock
boost::posix_time::second_clock能够获取本地时钟或者UTC时钟:
1
2
3
4
5
6
7
8namespace bpt = boost::posix_time;
auto utime = bpt::second_clock::universal_time();
auto ltime = bpt::second_clock::local_time() ;
cout << "utime: " << utime
<< "\nltime: " << ltime << "\n";
//输出:
// utime: 2026-Nov-04 08:25:56
// ltime: 2026-Nov-04 16:25:56
boost::posix_time转换到字符串、time_t
1 | namespace bpt = boost::posix_time; |
boost::posix_time::time_period
不同于time_t和std::time_t,boost::posix_time区分了时间点和时间区间的概念,时间区间一般是左闭右开区间,用法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace bpt = boost::posix_time;
auto begin = bpt::time_from_string("2026-10-31 00:00:00");
auto end = bpt::time_from_string("2026-11-01 00:00:00");
bpt::time_period period(begin, end);
//bpt::time_period period(begin, bpt::hours(24)); //或者直接指定24小时
auto mytime = bpt::time_from_string("2026-10-31 00:00:00");
auto mytime1 = bpt::time_from_string("2026-11-01 00:00:00");
auto mytime2 = bpt::time_from_string("2026-10-31 23:59:59");
cout << period.contains(mytime) << "\n" //true
<< period.contains(mytime1) << "\n" //false
<< period.contains(mytime2) << "\n"; //true
boost::posix_time::time_duration
boost::posix_time::time_duration用于计算两个ptime对应间隔时间,注意,
1
2
3
4
5
6namespace bpt = boost::posix_time;
auto begin = bpt::time_from_string("2026-10-31 00:00:00");
auto mytime = bpt::time_from_string("2026-10-30 00:00:00");
bpt::time_duration duration = mytime - begin;
cout << duration.hours() << endl; //-24, 小时数差24
cout << duration.minutes() << endl; //0,分钟数没差
boost::date_time::c_local_adjustor
boost::date_time::c_local_adjustor会自动根据本地市区,将ptime加上或者减去时区偏差再返回一个新的ptime,再强调一次,c_local_adjustor无法知道ptime原来是否本地时间,因为ptime本身不区分,因此如果传入本地ptime再调用c_local_adjustor,会导致时间计算了双重时区!
1
2
3
4
5
auto utime = bpt::second_clock::universal_time(); //2026-Nov-04 08:53:22
bpt::ptime local_time = boost::date_time::c_local_adjustor<bpt::ptime>::utc_to_local(utime);
cout << local_time << endl; //2026-Nov-04 16:53:22
参考链接:
