4.3 时间和日期
通常能确定时间和日期对一个程序来说是非常有用的。程序可能希望记录它运行的时间,或者可能需要在某些时候改变它的运行方式。例如,一个游戏可能拒绝在工作时间运行,或者一个定时备份程序可能想等到每天的凌晨才开始一个自动备份。
所有的UNIX系统都使用同一个时间和日期的起点:格林尼治时间(GMT)1970年1月1日午夜(0点)。这是“UNIX纪元的起点”, Linux也不例外。Linux系统中所有的时间都以从那时起经过的秒数来衡量。这和MS-DOS处理时间的方法类似,只是MS-DOS纪元始于1980年。其他系统使用其他的纪元起始时间。
时间通过一个预定义的类型time_t来处理。这是一个大到能够容纳以秒计算的日期和时间的整数类型。在Linux系统中,它是一个长整型,与处理时间值的函数一起定义在头文件time.h中。
绝不要想当然地以为,时间就是32位的。在使用32位time_t类型的UNIX和Linux系统中,时间将在2038年回绕。到那时,我们希望系统都开始使用大于32位的time_t类型。随着最近64位处理器进入主流处理器市场,这一趋势几乎是必然的。
你可以通过调用time函数得到底层的时间值,它返回的是从纪元开始至今的秒数。如果tloc不是一个空指针,time函数还会把返回值写入tloc指针指向的位置。
实验time函数
下面这个简单的程序envtime.c演示了time函数的用法:
运行这个程序,它会在20秒时间内每两秒钟打印一次底层的时间值。
实验解析
这个程序用一个空指针参数调用time函数,返回以秒数计算的时间和日期。程序休眠两秒后再重复调用time函数,总共调用10次。
以从1970年开始计算的秒数来表示时间和日期,对测算某些事情持续的时间是很有用的。你可以把它考虑为简单地把两次调用time得到的值相减。然而ISO/ANSI C标准委员会经过审议,并没有规定用time_t类型来测量任意时间之间的秒数,他们发明了一个函数difftime,该函数用来计算两个time_t值之间的秒数并以double类型返回它。
difftime函数计算两个时间值之间的差,并将time1-time2的值作为浮点数返回。对Linux来说,time函数的返回值是一个易于处理的秒数,但考虑到最大限度的可移植性,你最好使用difftime。
为了提供(对人类)更有意义的时间和日期,你需要把时间值转换为可读的时间和日期。有一些标准函数可以帮我们做到这一点。
gmtime函数把底层时间值分解为一个结构,该结构包含一些常用的成员:
tm结构被定义为至少包含表4-2所示的成员。
表4-2
tm_sec的范围允许临时闰秒或双闰秒。
实验gmtime函数
下面这个程序gmtime.c利用tm结构和gmtime函数打印出当前时间和日期:
运行这个程序,你将得到含义明显的时间和日期:
实验解析
这个程序调用time函数得到底层的时间值,然后调用gmtime将该值转换为一个包含有用的时间和日期值的结构。最后,程序用printf将这些信息打印出来。严格来说,你不应该用这种方法打印原始时间值,因为我们并不能保证它在所有系统上都是long类型的值。我们在运行gmtime程序后立即运行date命令以比较它们的输出。
不过,这儿有个小问题。如果在格林尼治标准时间(GMT)之外的时区运行这个程序,或者所在的地方像本例中那样采用了夏令时,你会发现时间(可能还有日期)是不对的。这是因为gmtime按GMT返回时间(现在GMT被称为世界标准时间,或UTC)。Linux和UNIX这样做是为了同步全球各地的所有程序和系统。不同时区同一时刻创建的文件都会有相同的创建时间。要看当地时间,你需要使用localtime函数。
localtime函数和gmtime一样,除了它返回的结构中包含的值已根据当地时区和是否采用夏令时做了调整。如果把上面程序中的gmtime换成localtime,再编译运行一次,你就会看到正确的时间和日期了。
要把已分解出来的tm结构再转换为原始的time_t时间值,你可以使用mktime函数:
如果tm结构不能被表示为time_t值,mktime将返回-1。
为了得到更“友好”的时间和日期表示,像date命令输出的那样,你可以使用asctime函数和ctime函数:
asctime函数返回一个字符串,它表示由tm结构timeptr所给出的时间和日期。这个返回的字符串有类似下面的格式:
它总是这种长度为26个字符的固定格式。ctime函数等效于调用下面这个函数:
它以原始时间值为参数,并将它转换为一个更易读的本地时间。
实验ctime函数
在本例中,使用下面的代码来查看ctime函数的用法:
编译并运行这个ctime.c程序,你将看到如下所示的输出:
实验解析
ctime.c程序调用time函数得到底层时间值,让ctime做所有的艰巨工作,把时间值转换成可读的字符串,然后打印它。
为了对时间和日期字符串的格式有更多控制,Linux和现代的类UNIX系统提供了strftime函数。它很像是一个针对时间和日期的sprintf函数,工作方式也很类似:
strftime函数格式化timeptr指针指向的tm结构所表示的时间和日期,并将结果放在字符串s中。字符串被指定(至少)maxsize个字符长。format字符串用于控制写入字符串s的字符。与printf一样,它包含将被传给字符串的普通字符和用于格式化时间和日期元素的转换控制符。转换控制符见表4-3。
表4-3
因此,date命令输出的普通日期就相当于strftime格式字符串中的:
为了读取日期,你可以使用strptime函数,该函数以一个代表日期和时间的字符串为参数,并创建表示同一日期和时间的tm结构:
format字符串的构建方式和strftime的format字符串完全一样。strptime在字符串扫描方面类似于sscanf函数,也是查找可识别字段,并把它们写入对应的变量中。只是这里是根据format字符串来填充tm结构的成员。不过,strptime的转换控制符与strftime的相比,限制要稍微松一些,因为strptime中的星期几和月份用缩写和全称都行,两者都匹配strptime中的%a控制符,此外,strftime对小于10的数字总以0开头,而strptime则把它看作是可选的。
strptime返回一个指针,指向转换过程处理的最后一个字符后面的那个字符。如果碰到不能转换的字符,转换过程就在该处停下来。调用程序需要检查是否已从传递的字符串中读入了足够多的数据,以确保tm结构中写入了有意义的值。
实验strftime函数和strptime函数
请留意下面这个程序中选用的转换控制符:
编译并运行这个程序strftime.c,你将得到:
实验解析
strftime程序通过调用time和localtime得到当前的本地时间。然后,它通过调用带有合适的格式参数的strftime将时间转换成可读的格式。为演示strptime的用法,程序构建了一个包含日期和时间的字符串,然后调用strptime将原始时间和日期值提取并打印出来。转换控制符%R是strptime中对%H:%M的缩写形式。
注意:要成功地扫描日期,strptime需要一个准确的格式字符串,这一点非常重要。一般来说,该函数不会准确扫描读自用户的日期,除非用户输入的格式非常严格。
编译strftime.c时,你可能会看到编译器有一个警告信息。这是因为GNU库在默认情况下并未声明strptime函数。要解决这个问题,你需要明确请求使用X/Open的标准功能,这需要在包含time.h头文件之前加上如下一行: