获取当前时间,把本地时间转为 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 和其他时间格式间的相互转换。

<w,700px>

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_rlocaltime_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 代表英语缩写的星期。所有格式化控制符可以在 此处 找到。

字符串转为分解时间

strptimestrftime 是一对互逆的函数,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;