C 语言中的时间处理函数
获取当前时间,把本地时间转为 UTC 时间,格式化时间,字符串转为时间,有很多时间相关的函数来完成这些工作,但弄清它们之间的关系需要费点功夫。因为对细节缺少了解,最近踩了一个时间相关的坑,于是开始仔细学习一下这部分内容。今天花时间看了下 《UNIX系统编程手册》的第十章,这一章专门讲时间相关的函数,看完之后豁然开朗,困扰我的问题也迎刃而解了。下面是我看书时候记的笔记,同时包含我的一些补充。
日历时间
Unix 系统内部使用自 Epoch 以来的经过的秒数来表示日历时间。Epoch 是通用协调时间(UTC)的 1970-01-01 00:00:00。日历时间存储于 time_t
类型中。
注意:UTC 和格林威治时间(GMT)是一样的。
#include <time.h>
time_t time(time_t *timep);
time
返回自 Epoch 至调用时刻的秒数。这个结果无论处在什么时区,全都是一样的。如果参数 timep
是有效指针,还会把结果写入此指针所指位置。
通常采用如下方式调用:
time_t now = time(nullptr);
time
返回的时间只能精确到秒,系统调用 gettimeofday
也可以返回当前时间,而且可以精确到微秒。
但是微秒的精度取决于硬件架构。目前较新的架构通常能够保证微秒的精度。
#include <sys/time.h>
struct timeval{
time_t tv_sec; /* Seconds. */
suseconds_t tv_usec; /* Microseconds. */
};
int gettimeofday (struct timeval * __tv, timezone * __tz)
timeval
中用秒和微秒两个字段来存储当前时间,其中秒和 time
返回的值一样。第二个参数 timezone
为是时区信息,目前已经废弃了,传入 NULL
即可。
上面提到的这两个函数,可以获得自 Epoch 至今的经过的时间,但是实际中常常需要把它转换为字符串,或者得到 年份、星期等信息,或者把时间转换为人类可读的字符串。这是下面几节要谈的。
时间转换函数一览
Unix 环境中提供了一组函数来实现 time_t
和其他时间格式间的相互转换。
将 time_t
转换为可打印格式
ctime
可以把 time_t
转换为表示日期和时间的字符串。此函数会考虑当地时区和夏令时。即返回的是本地时间。
#include <time.h>
char *ctime (const time_t *timep);
time_t now = time(nullptr);
ctime(&now); // => "Sat Jul 4 11:59:12 2020\n\0"
ctime
返回的日期和时间,其格式是固定的。其返回静态分配的字符串,因此它是线程不安全的。
time_t
和分解时间之间的转换
分解时间格式
分解时间使用 tm
结构体来表示,其中记录了年月日时分秒等信息。
/* ISO C `broken-down time' structure. */
struct tm {
int tm_sec; /* Seconds. [0-60] (1 leap second) */
int tm_min; /* Minutes. [0-59] */
int tm_hour; /* Hours. [0-23] */
int tm_mday; /* Day. [1-31] */
int tm_mon; /* Month. [0-11] */
int tm_year; /* Year - 1900. */
int tm_wday; /* Day of week. [0-6] */
int tm_yday; /* Days in year.[0-365] */
int tm_isdst; /* DST. [-1/0/1]*/
long int tm_gmtoff; /* Seconds east of UTC. */
const char *tm_zone; /* Timezone abbreviation. */
};
其中 tm_isdst
需要解释一下。 DST 是 Daylight Saving Time 的缩写,字面意思是“节约阳光时间”,也叫夏令时。在夏天,天亮的早,天黑的
晚,有的国家在夏天,会把时间调快一个小时,即本来是早上 5 点,但当地时间已经 6 点了。这样早上人们就会早起一个小时,
而晚上,本来是 23 点,但是本地时间已经是 0 点了,人们觉得已经很晚了,所以就睡觉了。这样的好处是,可以尽可能
利用当地的日照时间,减少照明量,节约能源。当夏天过去了,可以把时间再调回去。在中国,90年代短暂地实行过一段时间的夏令时,不过后来被取消了。
可见,夏令时在时区的基础上又对时间做了调整,而且不同国家调整的量不同。这些处理时间的函数能够考虑时区和夏令时的影响。
time_t
转为分解时间
#include <time.h>
struct tm *gmtime (const time_t *timep);
struct tm *localtime (const time_t *timep);
gmtime
能够把 time_t
转为 GMT 分解时间。localtime
考虑时区和夏令时设置,返回本地时间。
上面这两个函数返回静态分配的 tm
指针,是线程不安全的,gmtime_r
和 localtime_r
是对应的可重入版本。
分解时间转为 time_t
#include <time.h>
time_t mktime (struct tm *tp);
mktime
把输入的分解时间视为本地时间,然后将其转为 time_t
,而且会修改 tm
结构。比如设置 tm.tm_sec = 122
,由于秒数上限为 60,
mktime
会把它修改为 2,同时增加在分钟上加 2。设置秒为 -1,会减小 1 分钟,同时设置秒为 59。
注意: mktime
会把传给它的参数 tm
视为本地时间,这一点一定要注意,我就在这一点踩了坑。详情请看文末 “我踩的坑” 一节。
分解时间与字符串之间的转换
分解时间转换字符串
asctime
可将分解时间转为字符串,它和 ctime
的输出格式相同。
#include <time.h>
char *asctime (const struct tm *tp);
如果想要把分解时间转换为一个自定义格式的字符串,可以使用 strftime
,该函数支持高度定制化的格式。
#include <time.h>
size_t strftime(char * outstr, size_t maxsize,
const char * format, const struct tm *tp);
用法如下:
time_t now = time(nullptr);
struct tm time_info{};
gmtime_r(&now, &time_info);
char buffer[50];
strftime(buffer, sizeof(buffer), "%a, %d %b %Y %T GMT", &time_info);
cout << buffer; // => "Sat, 04 Jul 2020 05:43:35 GMT\0"
其中 format
是规定好的,比如 %a
代表英语缩写的星期。所有格式化控制符可以在 此处 找到。
字符串转为分解时间
strptime
和 strftime
是一对互逆的函数,strptime
可以把时间字符串转换为分解时间。
#include <time.h>
char *strptime(const char * str, const char *format, struct tm *tp);
用法如下:
struct tm time_info{};
const char* buffer = "Sat, 04 Jul 2020 05:43:35 GMT\0"
strptime(buffer, "%a, %d %b %Y %T GMT", &time_info);
在转换为 tm
结构时,如果字符串中只包含部分信息,比如 “2020-07-04”,此时 tm
中其他字段的值
将不会被修改。因此,可以多次调用 strptime
来修改 tm
。比如先设置日期,然后设置时间。
我踩的坑
我有一个 GMT 时间的字符串,就是 HTTP request 里面的头部 If-Modified-Since: Sat, 04 Jul 2020 09:51:49 GMT
。我打算将这个时间转换为 time_t
。
首先是要 strptime
得到 tm
,然后调用 mktime
不就完了,这是我最初的想法。但后来发现得到的 time_t
总是比期望值小 8 小时。后来仔细研究了 mktime
的用法之后,才发现错误之处。
// 错误代码
std::string str = "Sat, 04 Jul 2020 09:51:49 GMT";
struct tm time_info{};
strptime(str, "%a, %d %b %Y %T GMT", &time_info);
time_t t = mktime(&time_info);
这里输入的 tm
实际上是 GMT 时间,但是 mktime
把它视为本地时间了。因此,mktime
首先要做的是把输入的 tm
转为它心目中的 GMT 时间,这就是出错的原因。因此,如果要把输入的 GMT 时间 tm
转为 time_t
,需要在返回值上加上合适的时区偏移。
在 time.h
中定义了一个全局变量 timezone
。在我的机器上,它的值为 -28800。因为中国处在东八区,比 GMT 时间快了 8 小时,即 28800
秒。因此,在本地时间加上一个 timezone
就是 GMT 时间了。
mktime
把本为 GMT 的时间当做本地时间对待了,它会加上一个 timezone
,试图把“本地时间”转为 GMT 时间,然后转为 time_t
。但因为输入的本就是 GMT 时间,为了抵消 mktime
加上的 timezone
,需要从结果中减去 timezone
。
把上面代码中最后一行做如下修改,问题迎刃而解:
time_t t = mktime(&time_info) - timezone;