C++日期与时间编程(C++11-C++17)
0x00 导言
日期和时间是在编程中常常接触到的东西,而C++也为程序员提供了强(复)大(杂)的日期和时间编程的相关支持。在C++20标准中,C++的chrono
库继C++11标准后也迎来了空前绝后大更新,但本文讨论的重点主要集中在C++11至C++17标准中日期和时间相关的内容,C++20标准中的chrono
新东西完全可以另开一个坑。
在翻了N篇cppreference标准、chrome爽吃我800Mb的内存之后,我果断决定把这次的学习心得记下来,以防以后全部忘光光(实在太多了.jpg)。本片学习心得纯新手向,求大佬轻喷。
0x01 概论
先给两个例子:
- 获取当前时间
1 | /** 实现1 **/ |
1 | /** 实现2 **/ |
- 计算时间差
1 | /** 实现1 **/ |
1 | /** 实现2 **/ |
上文的两个要求都分别给出了两种实现方法,那接下来要说的事情已经显而易见了:
C++给出了两类日期时间API,分别是:
- C-Style日期时间库,主要位于
<ctime>
头文件(原C语言<time.h>
头文件的C++版本)中。 chrono
库:C++11新增的API。
在C++11引入chrono
库之前,C++程序员只能使用C-Style的日期时间库。然而,C-Style日期时间库有着鲜明的缺点:精度只有秒级别(当时),这对于对时间有着高精度要求的程序来说是完全不够用的。而C++11引入的chrono
库解决了这个问题,它极大地扩展了对精度的支持。当然,针对习惯C-Style日期时间库的程序员来说,C++17也进行了精度的扩充,详情请见下文。
本文也会分为C-Style和chrono
两个板块进行讲解。
0x02 必备的知识
在正式进入日期时间库的讨论之前,我们应该先复习一下与日期时间相关的必要知识。
UTC时间
协调世界时(Coordinated Universial Time,简称UTC)是最主要的时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林威治标准时间。
协调世界时是世界上调节时钟和时间的主要时间标准,它与0度经线的平太阳时相差不超过1秒。因此UTC时间+8即可获得北京标准时间(UTC+8)。
本地时间
本地时间与当地的时区相关,例如中国当地时间采用了北京标准时间(UTC+8).
纪元时间
纪元时间(Epoch time)又被称为Unix时间(常用Linux的小伙伴可能会比较熟悉)。它表示1970年1月1日00:00UTC以来所经历的秒数(不考虑闰秒)。
例如北京时间2021年5月18日晚上9点07分32秒的纪元时间为:1621372052
。
作为一个敏感的CPP程序员,你应该很快就意识到这个大整数在储存会产生很多问题,例如溢出。然而事实正是如此,在一些历史机器上,使用了32位的有符号整数来储存这个时间戳,因此产生在结果就是:在2038-01-19 03:14:07这一刻,该值会溢出。
另外,如果你对为什么选1970-1-1日零点做位纪元时间起点,可以看一下这个帖子:
Why is 1/1/1970 the “epoch time”?
0x03 C-Style 日期时间库
C-Style日期时间库主要位于<ctime>
头文件中,下面给出头文件中包含的常用的类型和函数。
常用类型与函数
类型
类型名 | 说明 |
---|---|
clock_t | 进程运行时间 |
size_t | sizeof运算符返回的无符号整数类型 |
time_t | 从纪元起的时间类型 |
tm | 日历时间类型 |
timespec*(C++17)* | 以秒和纳秒表示的时间(C++17) |
函数
函数名 | 说明 |
---|---|
std::clock_t clock() | 返回自程序启动时起的原始处理器时钟时间 |
std::time_t time(std::time_t* arg) | 返回自纪元起计的系统当前时间 |
double difftime(std::time_t time_end, std::time_t time_beg) | 计算时间之间的差 |
int timespec_get(std::timespec* ts, int base)(C++17) | 返回基于给定时间基底的日历时间(C++17) |
char* ctime(const std::time_t* time) | 转换 time_t 对象为文本表示 |
char* asctime(const std::tm* time_ptr) | 转换 tm 对象为文本表示 |
std::size_t strftime(char* str, std::size_t count, const char* format, const std::tm* time) | 转换 tm 对象到自定义的文本表示 |
std::size_t wcsftime( wchar_t* str, std::size_t count, const wchar_t* format, const std::tm* time) | 转换 tm 对象为定制的宽字符串文本表示 |
std::tm* gmtime(const std::time_t* time) | 将time_t转换成UTC表示的时间 |
std::tm* localtime(const std::time_t *time) | 将time_t转换成本地时间 |
std::time_t mktime(std::tm* time) | 将tm格式的时间转换成time_t表示的时间 |
函数和数据类型又多又杂有木有,刚开始接触的时候很容易弄混且不太容易记住,这里借鉴一下大佬的一张图来理解记忆:
这幅图中,以数据类型为中心,带方向的实心箭头表示该函数返回相应类型的结果。
- clock函数比较特别,它返回进程运行的时间,因而是相对独立的。
- time_t描述纪元时间(精确到秒),使用time函数获得。
- timespec类型在time_t的基础上增加了纳秒的精度,需要通过timespec_get函数获取。该类型与函数为C++17新增内容。
- tm是日历类型,包含了年月日等信息。可以通过gmtime,localtime和mktime函数进行time_t和tm类型的相互转化。
- gmtime和localtime两个函数存在时区差异相关问题。
- time_t和tm结构都可以用字符串格式输出。ctime和asctime输出的格式是固定的,如果需要自定义格式,需要使用strftime或者wcsftime函数。
计算进程运行的时间
clock函数会返回从关联到进程开始执行的实现定义时期的起,进程所用的粗略处理器时间。将此值除以CLOCKS_PER_SEC
常量可转换为秒。
这里给出cppreference上clock函数示例的简化版:
1 |
|
可能的输出:
1 | CPU time used (per clock(): 1580.00 ms |
我们应当注意,clock时间或许会快于或慢于挂钟时间,这取决于操作系统给予程序的执行资源。在单处理器的情况下,若CPU为其他进程所共享,clock可能慢于挂钟,若当前进程为多线程,而有更多资源可用,clock时间可能会快于挂钟。在多处理器情况下,若进程使用了多线程,那么clock时间可能要慢于挂钟。
获取纪元时间
使用time函数可以获取储存于time_t类型返回值里的纪元时间。
示例:
1 | time_t epoch_time = time(nullptr); |
在北京时间2021年5月18日晚上9点07分32秒输出如下:
1 | Epoch time: 1621372052 |
计算时间差
在一些情况下我们需要计算操作所花费的时间长度。可以看出,time_t结构中储存的是时间点,而通过常识我们得知:
1 | 时间差(时间长度) = 时间点 - 时间点 |
在C-Style日期时间库中,我们可以通过difftime函数来计算两个时间点的差。
下面给出cppreference的示例:
1 |
|
可能的输出如下:
1 | Wall time passed: 7 s. |
UTC时间与本地时间
在C-Style日期时间库中,我们可以使用gmtime将std::time_t的纪元时间转换为UTC时间,使用localtime将纪元时间转换为本地时区所代表的日历时间。
gmtime与localtime返回值的类型为tm结构,即日历时间的结构描述,其结构如下:
1 | struct tm { |
有两点我们需要注意:
- tm_mon表示的范围为[0, 11]。转换成日常使用的月份表示需要+1。
- tm_year表示的是自1900年之后所过的年份数。转换成日常使用的年份表示需要+1900。
下面给出示例:
1 | time_t now = time(nullptr); |
输出如下:
1 | gmtime: Tue May 18 21:36:48 2021 |
输出时间和日期
获取了时间,我们自然想要将时间以字符串的形式打印出来。此时可以使用ctime函数。
应注意的是,ctime函数打印的格式是固定的:
1 | Www Mmm dd hh:mm:ss yyyy\n |
示例:
1 | time_t now = time(nullptr); |
输出:
1 | Now is: Tue May 18 21:39:46 2021 |
而tm储存的日期时间结构,我们也可以使用asctime函数将其转换为字符串格式。
然而,ctime和asctime函数其输出的格式都是固定的,在有格式要求的情况下,我们通常有两种做法:
拆分tm结构体的字段:
1
2
3
4
5time_t now = time(nullptr);
tm* t = localtime(&now);
cout << "Now is: " << t->tm_year + 1900 << "/" << t->tm_mon + 1<< "/" << t->tm_mday << " ";
cout << t->tm_hour << ":" << t->tm_min << ":" << t->tm_sec << endl;使用strftime或者wcsftime函数来指定格式输出。函数格式可以参考以下文档:
https://zh.cppreference.com/w/cpp/chrono/c/strftime
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
std::locale::global(std::locale("ja_JP.utf8"));
std::time_t t = std::time(NULL);
char mbstr[100];
if (std::strftime(mbstr, sizeof(mbstr), "%A %c", std::localtime(&t))) {
std::cout << mbstr << '\n';
}
}示例输出:
1
火曜日 2011年12月27日 17時39分03秒
C++17新内容timespec,迈向纳秒精度
纪元时间的精度只有秒,这在很多时候是不够用的。为了解决这个问题,C++17增加timespec类型,提供了纳秒级别的精度。
一些熟悉C语言的朋友可能会立即满头问号:timespec不是在C11(注意看,是C11,不是CPP11)就已经引入了么?
但事实的确如此,严格的C++的C-Style日期时间库,在C++17标准才正式引入timespec类型。
timespec类型结构如下:
1 | struct timespec { |
其中tv_nsec成员保存了纳秒数。
示例:
1 | timespec ts; |
输出:
1 | Current time: 05/18/21 13:50:58.667861100 UTC |
C-Style日期时间库至此已基本结束,接下来讲解C++11起新增的chrono库。
0x04 chrono库
chrono是以各种精度跟踪时间的类型的灵活汇集。chrono库定义三种主要类型以及工具函数和常用的typedef:
- 时钟
- 时长
- 时间点
时钟
C++11的chrono库主要包含了三种类型的时钟:
名称 | 说明 |
---|---|
system_clock | 来自系统范畴实时时钟的挂钟时间 |
steady_clock | 决不会调整的单调时钟 |
high_resolution_clock | 拥有可用的最短嘀嗒周期的时钟 |
system_clock来源是系统时钟。然而在大多数系统上,系统时间是可以在任何时候被调节的。所以如果用来计算两个时间点的时间差,这并不是一个好的选择。
steady_clock是一个单调时钟。此时钟的时间点无法减少,因为物理十几件向前移动。因而steady_clock是度量间隔的最适宜的选择。
high_resolution_clock表示实现提供的拥有最小计次周期的时钟。它可以是system_clock或steady_clock的别名,或者第三个独立时钟。
对于high_resolution_clock,cppreference给出了以下需要我们注意的地方:
https://zh.cppreference.com/w/cpp/chrono/high_resolution_clock
high_resolution_clock
在不同标准库实现之间实现不一致,而应该避免使用它。通常它只是 std::chrono::steady_clock 或 std::chrono::system_clock 的别名,但实际是哪个取决于库或配置。它是system_clock
时不是单调的(即时间能后退)。例如对于 gcc 的 libstdc++ 它是system_clock
,对于 MSVC 它是steady_clock
,而对于 clang 的 libc++ 它取决于配置。通常用户应该直接使用 std::chrono::steady_clock 或 std::chrono::system_clock 代替
std::chrono::high_resolution_clock
:对时长度量使用steady_clock
,对壁钟时间使用system_clock
。
对于这三个时钟类,有着以下共同的成员:
名称 | 说明 |
---|---|
now() | 静态成员函数,返回当前时间,类型为clock::time_point |
time_point | 成员类型,当前时钟的时间点类型。 |
duration | 成员类型,时钟的时长类型。 |
rep | 成员类型,时钟的tick类型,等同于clock::duration::rep |
period | 成员类型,时钟的单位,等同于clock::duration::period |
is_steady | 静态成员类型:是否是稳定时钟,对于steady_clock来说该值一定是true |
每个时钟类都有着一个静态成员函数new()
来获取当前时间。该函数的返回类型则是由该时钟类的time_point描述,例如std::chrono::time_point<std::chrono::system_clock>
或者std::chrono::time_point<std::chrono::steady_clock>
。我们可以使用auto关键字来简写(auto是个好文明)。
阅读文档,我们不难发现system_clock有着与另外两个clock所不具有的特性:它是唯一有能力映射其时间点到C-Style时间的C++时钟。system_clock提供了两个静态成员函数来与std::time_t进行互相转换:
名称 | 说明 |
---|---|
to_time_t | 转换系统时钟时间点为 std::time_t |
from_time_t | 转换 std::time_t 到系统时钟时间点 |
为了方便理解和记忆,我们也用一副图来描述几种时间类型的转换:
时长
人类对精度的要求永无止境。C-Style日期时间库为了提供对纳秒的精度,增加了timespec类型及相关的函数。那如果以后对更高精度的需求越来越高,C++标准库还要不断增加更多的类型和配套函数吗?这明显是一个不合理的设计。因而C++标准提出了一个新的解决思路,而这个思路涉及到了C++11引入的一个新的头文件和类型:ratio。
ratio
std::ratio
定义在<ratio>
文件中,提供了编译期的比例计算功能。
std::ratio
的定义如下:
1 | template< |
类成员Num即为分子,类成员Denom即为分母。我们可以直接通过调用类成员来获取相关值。
<ratio>
头文件还包含了:ratio_add,ratio_subtract,ratio_multiply,ratio_divide来完成分数的加减乘除四则运算。
例如,想要计算5/7 + 59/1023,我们可以这样写:
1 | ratio_add<ratio<5, 7>, ratio<59, 1023>> result; |
对于编译期有理数算数的相关内容,可以在这篇文档中找到更多信息:
https://zh.cppreference.com/w/cpp/numeric/ratio
有了ratio之后,结合std::chrono::duration
,我们便可以表示任意精度的值了。
例如,相对于秒来说,毫秒是1/1,000,微秒是1/1,000,000,纳秒是1/1,000,000,000。通过ratio就可以这样表达:
1 | std::ratio<1, 1000> milliseconds; |
时长类型
类模板 std::chrono::duration 表示时间间隔。有了ratio之后,表达时长就很方便了,下面是chrono库中提供的很常用的几个时长单位:
类型 | 定义 |
---|---|
std::chrono::nanoseconds | duration</*至少 64 位的有符号整数类型*/, std::nano> |
std::chrono::microseconds | duration</*至少 55 位的有符号整数类型*/, std::micro> |
std::chrono::milliseconds | duration</*至少 45 位的有符号整数类型*/, std::milli> |
std::chrono::seconds | duration</*至少 35 位的有符号整数类型*/> |
std::chrono::minutes | duration</*至少 29 位的有符号整数类型*/, std::ratio<60» |
std::chrono::hours | duration</*至少 23 位的有符号整数类型*/, std::ratio<3600» |
我们可以调用duration类的count()
成员函数来获取具体数值。
时长运算
时长运算可以直接使用“+”或“-”相加相减。chrono库也提供了几个常用的函数:
函数 | 说明 |
---|---|
duration_cast | 进行时长的转换 |
floor(C++17) | 以向下取整的方式,将一个时长转换为另一个时长 |
ceil(C++17) | 以向上取整的方式,将一个时长转换为另一个时长 |
round(C++17) | 转换时长到另一个时长,就近取整,偶数优先 |
abs(C++17) | 获取时长的绝对值 |
例如:想要知道2个小时零5分钟一共是多少秒,可以这样写:
1 | chrono::hours two_hours(2); |
我们可以得到:
1 | 02:05 is 7500 seconds |
从C++14开始,你甚至可以用字面值来描述常见的时长。这包括:
h
表示小时min
表示分钟s
表示秒ms
表示毫秒us
表示微妙ns
表示纳秒
这些字面值位于std::chrono_literals
命名空间下。于是,可以这样表达2个小时以及5分钟:
1 | using namespace std::chrono_literals; |
时间点
时钟的now函数返回的值就是一个时间点,时间点包含了时钟和时长两个信息。
类模板std::chrono::time_point
表示时间中的一个点,定义如下:
1 | template< |
与我们的常识一致,时间点具有加法和减法操作:
1 | 时间点 + 时长 = 时间点 |
因而我们可以通过两个时间点相减来计算一个时间间隔,示例如下:
1 | auto start = chrono::steady_clock::now(); |
两个时间点也存在着比较操作,用于判断一个时间点在另外一个时间点之前还是之后,std::chrono::time_point
重载了==
,!=
,<
,<=
,>
,>=
操作符来实现比较操作。