C&C++时间操作
crazyang Lv1

UNIX 时间概念

在 UNIX 系统中,将从 1970 年 1 月 1 日开始经过的秒数用一个整数存放,这种高效简洁的时间表示方法被称为 Unix 时间戳,向左和向右偏移都可以得到更早或者更后的时间。实际开发中,对日期和时间的操作非常多,基本无处不在。

时间的概念:

  • 本地时间(locale time)

    本地时间是在纪元时间(UTC)时间上加上时区。

  • 格林威治时间(Greenwich Mean Time GMT

    世界时是最早的时间标准。在1884年,国际上将 1s 确定为全年内每日平均长度的 1/8.64×104 。以此标准形成的时间系统,称为世界时,即UT1。1972 年国际上开始使用国际原子时标,从那以后,经过格林威治老天文台本初子午线的时间便被称为世界时,即UT2,或称格林威治时间(GMT),是对地球转速周期性差异进行校正后的世界时。

  • 世界协调时间 (Universal Time Coordinated UTC

    世界协调时(也有称为纪元时间)是以地球自转为基础的时间标准。由于地球自转速度并不均匀,并非每天都是精确的 86400 原子 s,因而导致了自转时间与世界时之间存在 18 个月有 1s 的误差。为纠正这种误差,国际地球自转研究所根据地球自转的实际情况对格林威治时间进行增减闰 s 的调整,与国际度量衡局时间所联合向全世界发布标准时间,这就是所谓的世界协调时。UTC 的表示方式为:年(y)、月(m)、日(d)、时(h)、分(min)、秒(s),均用数字表示。

GPS 系统中有两种时间区分,一为UTC,另一为LT(地方时)。两者的区别为时区不同,UTC就是 0 时区的时间,地方时为本地时间,如北京为早上八点(东八区),UTC 时间就为零点,时间比北京时晚八小时,以此计算即可通过上面的了解,我们可以认为格林威治时间就是世界协调时间(GMT = UTC),格林威治时间和 UTC 时间均用秒数来计算的。

而在我们平时工作当中看到的计算机日志里面写的时间大多数是用UTC时间来计算的,那么我们该怎么将UTC时间转化为本地时间便于查看日志,那么在作程序开发时又该怎么将本地时间转化为UTC时间呢?
可以使用 Linux 命令 date 来进行本地时间和 local 时间的转化。大家都知道,在计算机中看到的 UTC 时间都是从(1970 年 01 月 01 日 0:00:00)开始计算秒数的。所看到的UTC时间那就是从1970年这个时间点起到具体时间共有多少秒。

C 语言的时间操作函数

time_t 类型

C 语言中,用 time_t 表示一个时间类型,实际 long int 类型的的别名。表示一个日历时间,是从 1970 年 1 月1日 0 时 0 分 0 秒到现在的秒数,也称为时间戳。

time 函数

time 函数的用途是返回现在的时间戳,也就是从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的秒数。time 函数是 C 语言标准库中的函数,在 time.h 中声明,函数原型如下:

1
time_t time(time_t* t);

time_t 函数有两种调用方法:

1
2
3
4
time_t tnow;
tnow = time(NULL);
//或者
time(&tnow)

这两种效果完全相同,如下实例:

1
2
3
4
5
6
7
8
9
int main()
{
time_t tnow;
tnow = time(NULL);
cout << tnow << endl;
time(&tnow);
cout << tnow << endl;
return 0;
}

运行结果:

1
2
3
$ ./test 
1616748809
1616748809

struct tm 结构体

time_t 只是一个长整型,仅仅表示一个时间戳,不符合我们的使用习惯,需要转换成可以方便表示时间信息的结构体,这正是 struct tm 结构体的作用。struct time 结构体在 time.h 中声明,如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct tm
{
int tm_sec; // Seconds. 秒:取值区间为[0,59]
int tm_min; // Minutes. 分:取值区间为[0,59]
int tm_hour; // Hours. 时:取值区间为[0,23]
int tm_mday; // Day. 日期:一个月中的日期:取值区间为[1,31]
int tm_mon; // Month. 月份:(从一月开始,0代表一月),取值区间为[0,11]
int tm_year; // Year - 1900. 年份:其值等于实际年份减去1900
int tm_wday; // Day of week. 星期:取值区间为[0,6],0代表星期天,1代表星期一
int tm_yday; // Days in year. 从每年的1月1日开始的天数:取值区间为[0,365],其中0代表1月1日,1代表1月2日,以此类推
int tm_isdst; /* DST. 夏令时标识符,该字段意义不大,很少用到
};

此结构体中定义了年、月、日、时、分、秒、星期、当年中的某一天、夏令时。struct tm 结构体可以很方便的显示出一个时间的详细信息。

localtime 函数

localtime 函数用于将 time_t 表示的时间戳转换为 struct tm 结构体表示的时间,函数返回 struct tm 结构体的地址。

struct tm 结构体包含了时间的各要素,但还一定是我们习惯的时间表达方式,可以用格式化输出 printf 等函数,把 struct tm 结构体转换为我们想要的结果。

1
2
3
4
5
6
7
8
9
10
int main()
{

time_t tnow;
tnow = time(NULL);
printf("tnow=%lu\n", tnow); // 输出时间戳
struct tm* sttm = localtime(&tnow);
printf("%04u-%02u-%02u %02u:%02u:%02u\n", sttm->tm_year + 1900, sttm->tm_mon + 1, sttm->tm_mday, sttm->tm_hour, sttm->tm_min, sttm->tm_sec);
return 0;
}

运行结果:

1
2
3
$ ./test 
tnow=1616749813
2021-03-26 17:10:13

在实际开发中,通常需要获取十分钟之后的时间,方法是采用 time 函数得到一个整数后,再加上 10*60 秒,再用 localtime 函数转换为结构体即可。

mktime 函数

mktime 函数的功能与 localtime 函数相反。所以,mktime 函数用于把 struct tm 表示的时间转换为 time_t 表示的时间,其函数声明如下:

1
time_t mktime(struct tm* tm);

用法如下实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
struct tm sttm;
bzero(&sttm, sizeof(sttm));

sttm.tm_year = 2021 - 1900;
sttm.tm_mon = 3 - 1;
sttm.tm_mday = 25;
sttm.tm_hour = 17;
sttm.tm_min = 15;
sttm.tm_sec = 45;
sttm.tm_isdst = 0;

time_t tnow = mktime(&sttm);
cout << tnow << endl;
return 0;
}

运行结果:

1
2
$ ./test 
1616663745

gettimeofday 函数

gettimeofday 是获得 1970 年 1 月 1 日 到当前时间的秒和微秒,微秒是指当前秒已逝去的微秒数,可以用于程序的计时。调用 gettimeofday 函数需要包含 sys/time.h 头文件。gettimeofday 函数在 windows 平台中不能使用。

在 Linux 中,精确到微秒的 timeval 结构体定义如下:

1
2
3
4
5
struct timeval
{
long tv_sec; // 1970年1月1日到现在的秒。
long tv_usec; // 当前秒的微妙,即百万分之一秒。
};

gettimeofday 函数中使用到了 timezone 结构体,其在 sys/time.h 文件中定义:

1
2
3
4
5
struct timezone
{
int tz_minuteswest; // 和UTC(格林威治时间)差了多少分钟。
int tz_dsttime; // type of DST correction,修正参数据,忽略
};

gettimeofday 函数的声明如下:

1
int gettimeofday(struct timeval* tv, struct timezone* tz )

参数:tv 用来存放当前的时间,tz 用来存放当地时区的信息,tz 不关心可以为 NULL。

返回值:执行成功返回 0,失败后返回 -1。

实例程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/time.h>
int main()
{
struct timeval begin, end;
gettimeofday(&begin, NULL);
printf("begin time(0)=%d,tv_sec=%d,tv_usec=%d\n", time(0), begin.tv_sec, begin.tv_usec);
sleep(2);
usleep(100000);
gettimeofday(&end, 0);
printf("end time(0)=%d,tv_sec=%d,tv_usec=%d\n", time(0), end.tv_sec, end.tv_usec);
printf("计时过去了%d微秒\n", (end.tv_sec - begin.tv_sec) * 1000000 + (end.tv_usec - begin.tv_usec));
}

运行结果:

1
2
3
4
$ ./test 
begin time(0)=1616768189,tv_sec=1616768189,tv_usec=671573
end time(0)=1616768191,tv_sec=1616768191,tv_usec=771734
计时过去了2100161微秒

从运行结果看出实际时间大于十分之一秒?因为程序执行需要时间,虽然这个时间很短,在千分之一秒内。

asctime 函数

asctime 函数将结构体中的信息转换为真实世界中的时间(无时区转换),以字符串的形式显示。其定义如下:

1
2
#include <time.h>
char *asctime(const struct tm* timeptr);

程序实例如下:

1
2
3
4
5
6
7
8
#include <time.h>
int main()
{
time_t timep;
time(&timep);
printf("%s", asctime(gmtime(&timep)));
return 0;
}

运行结果:

1
2
$ ./test 
Fri Mar 26 14:23:21 2021

在上面的程序中,首先使用 gmtime 将 time_t 类型的时间转换为 struct tm 类型的时间,按没有经过时区转换的 UTC 时间,再用 asctime 转换为我们常见的格式,如:Fri Jan 11 17:25:24 2008。

ctime 函数

ctime 函数将一个 time_t 的时间戳转换为真实世界的时间(以字符串显示),和 asctime 不同在于传入的参数不一样。

1
char *ctime(const time_t* timep);

实例程序如下:

1
2
3
4
5
6
7
int main()
{
time_t timep;
time(&timep);
printf("%s", ctime(&timep));
return 0;
}

运行结果:

1
2
$ ./test 
Fri Mar 26 22:28:58 2021

可以看到运行结果和电脑时间一致,表示已经经过时区转换了。

gmtime 函数

gmtime 函数将一个 time_t 的时间戳转换为没有经过时区转换的 UTC 时间,是一个 struct tm 结构体指针:

1
struct tm* gmtime(const time_t* timep);

实例程序如下:

1
2
3
4
5
6
7
8
9
10
int main()
{
char* wday[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
time_t timep;
time(&timep);
struct tm *p = gmtime(&timep);
printf("%d/%d/%d ", 1900 + p->tm_year, 1 + p->tm_mon, p->tm_mday);
printf("%s %d:%d:%d\n", wday[p->tm_wday], p->tm_hour, p->tm_min, p->tm_sec);
return 0;
}

运行结果:

1
2
$ ./test 
2021/3/26 Fri 14:35:17

可以看到运行结果比当前电脑时间少 8 个小时,也就是没有经过时区转换

!!!注意:gmtime 和 localtime 函数都是线程不安全的,也就是不可重入的,在实际开发中,使用 gmtime_r 和 localtime_r 函数。

difftime 函数

difftime 函数用于返回两个时间相差的秒数,一般用于将两个 struct tm 转换为 time_t 后计算其相差的秒数。

函数原型如下:

1
double difftime(time_t time1, time_t time2);

示例程序:

1
2
3
4
5
6
7
8
int main()
{
struct timeval begin, end;
gettimeofday(&begin, NULL);
sleep(2);
gettimeofday(&end, 0);
cout << difftime(end.tv_sec, begin.tv_sec) << endl;
}

运行结果:

1
2
$ ./test 
2

strftime 函数

strftime() 函数将时间格式化,我们可以使用strftime()函数将时间格式化为我们想要的格式。它的原型如下:

1
2
3
4
5
6
size_t strftime(
char *strDest,
size_t maxsize,
const char *format,
const struct tm *timeptr
);

根据 format 指向字符串中格式命令把 timeptr 中保存的时间信息放在 strDest 指向的字符串中,最多向 strDest 中存放 maxsize 个字符。该函数返回向 strDest 指向的字符串中放置的字符数。

函数 strftime() 的操作有些类似于 sprintf()。识别以百分号 (%) 开始的格式命令集合,格式化输出结果放在一个字符串中。格式化命令说明串 strDest 中各种日期和时间信息的确切表示方法。格式串中的其他字符原样放进串中。格式命令列在下面,它们是区分大小写的。

格式 说明
%a 星期几的简写
%A 星期几的全称
%b 月分的简写
%B 月份的全称
%c 标准的日期的时间串
%C 年份的后两位数字
%d 十进制表示的每月的第几天
%D 月/天/年
%e 在两字符域中,十进制表示的每月的第几天
%F 年-月-日
%g 年份的后两位数字,使用基于周的年
%G 年分,使用基于周的年
%h 简写的月份名
%H 24小时制的小时
%I 12小时制的小时
%j 十进制表示的每年的第几天
%m 十进制表示的月份
%M 十时制表示的分钟数
%n 新行符
%p 本地的AM或PM的等价显示
%r 12小时的时间
%R 显示小时和分钟:hh:mm
%S 十进制的秒数
%t 水平制表符
%T 显示时分秒:hh:mm:ss
%u 每周的第几天,星期一为第一天(值从0到6,星期一为0)
%U 第年的第几周,把星期日做为第一天(值从0到53)
%V 每年的第几周,使用基于周的年
%w 十进制表示的星期几(值从0到6,星期天为0)
%W 每年的第几周,把星期一做为第一天(值从0到53)
%x 标准的日期串
%X 标准的时间串
%y 不带世纪的十进制年份(值从0到99)
%Y 带世纪部分的十制年份
%z,%Z 时区名称,如果不能得到时区名称则返回空字符。
%% 百分号

如果想显示现在是几点了,并以12小时制显示,就象下面这段程序:

1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
struct tm *ptr;
time_t lt;
char str[80];
lt = time(NULL);
ptr = localtime(&lt);
strftime(str, 100, "It is now %I %p\n", ptr);
printf(str);
return 0;
}

运行结果如下:

1
2
$ ./test 
It is now 10 PM

C++ 中时间操作函数

C++ 基本时间函数

C++ 标准库中很多实践操作函数是从 C 里来的,上面说的有些函数只能在 Linux/Unix 下使用,在 C++ 中进行了封装,使用和 C 语言的类似。

C++ 中比如:std::colck、std::chrono,获取的时间精度都比较高,同时也比较慢,比 std::time 的慢了 2~3 个数量级。

C++ 中类似于 C 的常用时间操作函数如下:

std::tm:类似于 C 中的 struct tm 函数

std::time:类似于 C 中的 time 函数

std::localtime:类似于 C 中的 localtime 函数

std::gmtime:类似于 C 中的 gmtime 函数

std::strfime:类似于 C 中的 strfime 函数

std::asctime:类似于 C 中的 asctime 函数(但已被弃用)

std::ctime:类似于 C 中的 ctime 函数(但已被启用)

std::clock 函数

clock() 函数,返回从开启这个程序进程到程序中调用 clock() 函数 时之间的 CPU 时钟计时单元(clock tick)数(挂钟时间),返回单位是毫秒。

clock() 函数返回类型为 clock_t 类型,clock_t 实际是 long int 类型,可以用常量 CLOCKS_PER_SEC,这个常量表示每一秒(per second)有多少个时钟计时单元。

实例程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
clock_t start, end;
start = std::clock();
//如果函数执行时间少的话,可能最后测出的结果为0,因为现在机子的运行速度很快
for (int j = 0; j < 1000; j++)
{
for (int i = 0; i < 1000000; i++)
{
}
}
end = std::clock();
cout << (double)(end - start) / CLOCKS_PER_SEC << endl;
return 0;
}

运行结果:

1
2
$ ./test 
2

std::chrono 库

chrono 是一个 time library,源于boost,现在已经是 C++ 标准。要使用chrono库,需要 #include<chrono>,其所有实现均在 std::chrono namespace 下。chrono 是一个模版库,使用简单,功能强大,只需要理解三个概念:duration、time_point、clock。

Durations

std::chrono::duration 表示一段时间,比如两个小时、12.88秒、半个时辰等等,只要能换算成秒即可。

1
template <class Rep, class Period = ratio<1> > class duration;

其中:
Rep表示一种数值类型,用来表示Period的数量,比如 int、float、double

Period是 ratio 类型,用来表示(用秒表示的时间单位)比如 second、milisecond

常用的 duration<Rep, Period> 已经定义好了,在 std::chrono::duration 下:

1
2
3
4
5
6
ratio<3600, 1> hours
ratio<60, 1> minutes
ratio<1, 1> seconds
ratio<1, 1000> microseconds
ratio<1, 1000000> microseconds
ratio<1, 1000000000> nanosecons

这里需要说明一下ratio这个类模版的原型:

1
template <intmax_t N, intmax_t D = 1> class ratio;

N代表分子,D代表分母,所以 ratio 表示一个分数值。

注意,我们自己可以定义Period,比如 ratio<1, -2> 表示单位时间是 -0.5 秒。

由于各种 duration 表示不同,chrono 库提供了 duration_cast 类型转换函数。

1
template <class ToDuration, class Rep, class Period> constexpr ToDuration duration_cast (const duration<Rep,Period>& dtn);

一段时间的典型用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <ratio>
#include <chrono>

int main()
{
typedef std::chrono::duration<int> seconds_type;
typedef std::chrono::duration<int, std::milli> milliseconds_type;
typedef std::chrono::duration<int, std::ratio<60 * 60>> hours_type;

hours_type h_oneday(24); // 24h
seconds_type s_oneday(60 * 60 * 24); // 86400s
milliseconds_type ms_oneday(s_oneday); // 86400000ms

seconds_type s_onehour(60 * 60); // 3600s
//hours_type h_onehour (s_onehour); // NOT VALID (type truncates), use:
hours_type h_onehour(std::chrono::duration_cast<hours_type>(s_onehour));
milliseconds_type ms_onehour(s_onehour); // 3600000ms (ok, no type truncation)

std::cout << ms_onehour.count() << "ms in 1h" << std::endl;

return 0;
}

duration 还有一个成员函数 count() 返回 Rep 类型的 Period 数量,看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream> // std::cout
#include <chrono> // std::chrono::seconds, std::chrono::milliseconds
// std::chrono::duration_cast
using namespace std::chrono;
int main()
{
// std::chrono::milliseconds is an instatiation of std::chrono::duration:
milliseconds foo(1000); // 1 second
foo *= 60;

std::cout << "duration (in periods): ";
std::cout << foo.count() << " milliseconds.\n";

std::cout << "duration (in seconds): ";
std::cout << foo.count() * milliseconds::period::num / milliseconds::period::den;
std::cout << " seconds.\n";

return 0;
}

Time points

std::chrono::time_point 表示一个具体时间,如上个世纪 80 年代、你的生日、今天下午、火车出发时间等,只要它能用计算机时钟表示。鉴于我们使用时间的情景不同,这个 time point 具体到什么程度,由选用的单位决定。一个 time point 必须有一个 clock 计时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <chrono>
#include <ctime>

using namespace std::chrono;

int main()
{
system_clock::time_point tp_epoch; // epoch value

time_point<system_clock, duration<int>> tp_seconds(duration<int>(1));

system_clock::time_point tp(tp_seconds);

std::cout << "1 second since system_clock epoch = ";
std::cout << tp.time_since_epoch().count();
std::cout << " system_clock periods." << std::endl;

// display time_point:
std::time_t tt = system_clock::to_time_t(tp);
std::cout << "time_point tp is: " << ctime(&tt);

return 0;
}

time_point 有一个函数 time_from_eproch() 用来获得 1970 年 1 月 1 日到 time_point 时间经过的 duration。
举个例子,如果 timepoint 以天为单位,函数返回的 duration 就以天为单位。

由于各种 time_point 表示方式不同,chrono 也提供了相应的转换函数 time_point_cast。

1
2
template <class ToDuration, class Clock, class Duration>
time_point<Clock,ToDuration> time_point_cast (const time_point<Clock,Duration>& tp);

比如计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <ratio>
#include <chrono>
using namespace std::chrono;

int main()
{
typedef duration<int, std::ratio<60 * 60 * 24>> days_type;
time_point<system_clock, days_type> today = time_point_cast<days_type>(system_clock::now());
std::cout << today.time_since_epoch().count() << " days since epoch" << std::endl;

return 0;
}

Clocks

std::chrono::system_clock 它表示当前的系统时钟,系统中运行的所有进程使用 now() 得到的时间是一致的。每一个 clock 类中都有确定的 time_point、duration、Rep、Period 类型。

操作有如下:

  • now() 当前时间 time_point

  • to_time_t() time_point 转换成 time_t 秒

  • from_time_t() 从 time_t 转换成 time_point

典型的应用是计算时间日期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <ctime>
#include <ratio>
#include <chrono>

int main()
{
using std::chrono::system_clock;

std::chrono::duration<int, std::ratio<60 * 60 * 24>> one_day(1);

system_clock::time_point today = system_clock::now();
system_clock::time_point tomorrow = today + one_day;

std::time_t tt;

tt = system_clock::to_time_t(today);
std::cout << "today is: " << ctime(&tt);

tt = system_clock::to_time_t(tomorrow);
std::cout << "tomorrow will be: " << ctime(&tt);

return 0;
}

std::chrono::steady_clock 为了表示稳定的时间间隔,后一次调用 now() 得到的时间总是比前一次的值大(这句话的意思其实是,如果中途修改了系统时间,也不影响 now() 的结果),每次 tick 都保证过了稳定的时间间隔。
操作有:

  • now() 获取当前时钟

典型的应用是给算法计时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <ctime>
#include <ratio>
#include <chrono>

int main()
{
using namespace std::chrono;

steady_clock::time_point t1 = steady_clock::now();

std::cout << "printing out 1000 stars...\n";
for (int i = 0; i < 1000; ++i)
std::cout << "*";
std::cout << std::endl;

steady_clock::time_point t2 = steady_clock::now();

duration<double> time_span = duration_cast<duration<double>>(t2 - t1);

std::cout << "It took me " << time_span.count() << " seconds.";
std::cout << std::endl;

return 0;
}

最后一个时钟,std::chrono::high_resolution_clock 顾名思义,这是系统可用的最高精度的时钟。实际上 high_resolution_clock 只不过是 system_clock 或者 steady_clock 的 typedef。

操作有:

  • now() 获取当前时钟。

参考连接:

  • 本文标题:C&C++时间操作
  • 本文作者:crazyang
  • 创建时间:2020-05-20 12:24:14
  • 本文链接:https://blog.codepeak.cn/2020/05/20/C&C++时间操作/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论