从上面我们可以看到,表达日历时间除了记录时间跨度之外还需要保存时区信息,然而我们的time_t并没有保存时区(timezone)!这是因为标准库把时区的设置交给了系统以及用户自己,在标准库里受到支持的只有local time和UTC time。
因此你会发现标准库函数都对参数是何种时间,返回值是什么时间做了明确的声明。而我们的mktime接受的是_local time_而返回的是_UTC time_,所以time_t所表示的时间比我们预想的差了8小时。
所以我们在Linux上处理时间时一定要注意上下文中时间值附带的时区信息。
带有完整日历信息的struct tm和time_t息息相关的要数struct tm了,它的声明如下:
struct tm { int tm_sec; /* 秒 [0-60] 允许有1秒的闰秒存在(c++11前和c99前允许2秒的闰秒,所以最大值是61) */ int tm_min; /* 分 [0-59] */ int tm_hour; /* 时 [0-23] */ int tm_mday; /* 日 [1-31] */ int tm_mon; /* 月,1月为0 */ int tm_year; /* 年份,从1900年开始计算,1970年的值为70 */ int tm_wday; /* 星期几,星期天为0,星期六为6,依次递增 */ int tm_yday; /* 一年中的第几天,1月1日是0 */ int tm_isdst; /* 是否启用夏令时 */ };当然这只是标准给出的必须要有的成员,实际上在某些bsd系统中struct tm实际上还会包含时区相关的成员,为了写出可移植的代码我们将这些附加内容视为不存在。
获取struct tm除了像我们上一节那样手动指定成员的值之外,还有若干标准库函数可供使用:
// mktime不再赘述,它除了转换tm到time_t之外还可以根据给出的字段自动将tm设置成合理的值 // localtime 认为收到的是local time,返回该local time对应的tm值 // 注意t1复制了返回值,因为localtime,gmtime返回的是static生命周期的指针,无法保证它的值不会被修改 std::time_t now = std::time(nullptr); std::tm t1 = *std::localtime(&now); // gmtime 认为接受的是local time,返回将该local time转换为UTC time之后的值 std::tm *t2 = std::gmtime(&now); // difftime用于比较两个time_t之间相差的秒数 auto time_end = mktime(&t1); auto time_beg = mktime(t2); std::cout << std::difftime(time_end, time_beg) << std::endl; // Output: 28800正如上面代码所示,标准库提供的函数gmtime, localtime, asctime, ctime都使用了函数内的static存储,所以必要的情况下必须把结果值进行拷贝;或者你也可以使用posix提供的带_r后缀的安全版本。
结果是28800秒,也就是8小时,我们所在的时区是UTC+8,符合预期。
此外我们还可以将tm进行格式化输出:
// ctime将接收的time_t视为UTC time,将其转换为local time之后再转换成字符串 // ctime相当于asctime(localtime(...)) std::time_t t1{}; // 默认初始化为0 std::cout << std::ctime(&t1) << std::endl; // Output: Thu Jan 1 08:00:00 1970 // asctime将收到的tm数据原样输出,不做任何时区的转换 std::tm tm1{}; tm1.tm_year = 70; tm1.tm_mday = 1; mktime(&tm1); std::cout << std::asctime(&tm1) << std::endl; // Output: Thu Jan 1 00:00:00 1970此外我们还有strftime和strptime(需要#define _XOPEN_SOURCE)用来将tm格式化为字符串和将字符串解析为tm,限于篇幅我们不过多介绍。
在看过这些常用接口之后,我觉得你现在一定陷入混乱了,因为每个函数对时区的假设都不同,甚至一个函数的参数和返回值的时区也不相同!这就是为什么在Linux上处理时间问题会成为噩梦的原因之一。
你可以靠下图进行简单的记忆,黄色线代表与时区无关,蓝色代表不进行时区转换,红色代表转换为local time,绿色则是UTC time:
至于local和UTC以外的时区怎么办。。。没办法,只能自己手动算时区偏移量了。
过时的timevaltimeval的声明如下:
#include <sys/time.h> struct timeval { time_t tv_sec; // 秒 suseconds_t tv_usec; // us 微秒 };前面两种方案精度只能到秒,而struct timeval可以存储到微秒。timeval除了表示日期类似于time_t之外,还可以用来表示时间跨度(duration):
#include <sys/time.h> // included by time.h #include <time.h> struct timeval t; (void)gettimeofday(&t, nullptr); // UTC // 使用timeval作为时间长度 struct timeval wait_time = {1, 500000}; // 1.5秒 select(NFDS, read_fds, write_fds, err_fds, &wait_time);gettimeofday的第二个参数是时区,然而在Linux和glibc上这个参数的实际意义是没有被定义的,所以我们传递nullptr。
由于gettimeofday自身的原因,你通常无法获取到足够到微秒的精度,会存在些许的偏差。另外posix1.2008已经将gettimeofday标记为废弃,因此我们不应该继续使用这一api,因此这里不做过多讨论。
使用timeval结构的函数也少的可怜,只有select和pselect。
更现代的timespectimespec的声明如下:
#include <time.h> struct timespec { time_t tv_sec; // 秒 long tv_nsec; // 纳秒 };struct timespec是更现代的精度也更高的结构,精度达到了纳秒。同时c11和c++17标准还将其纳入了标准库,因此它现在不再只是posix标准下的了。
获得timespec有两种途径,首先是c和c++标准库提供的方法,我们以c++为例(c的方法完全一样):
std::timespec ts; timespec_get(&ts, TIME_UTC);这样我们就获得了现在的UTC时间的值。第二个参数标准目前只定义了TIME_UTC,所以现在还无法直接获取其他时区的时间值。