c++日期库

c++直到20标准才提供了日期类型,在c++20以前只能使用c语言的相关接口,c语言的日期接口在头文件中ctime中,此外还有一个格式化日期的函数位于头文件iomanip中。

§ctime一览

下面是头文件ctime的部分声明,此处已经添加了一些注释用来简单说明结构体和函数的功能。

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 每秒的处理器始时钟周期数
#define CLOCKS_PER_SEC /* 实现定义 */

namespace std {
    // 进程运行时间,用于对代码进行计时
    using clock_t = /* 实现定义 */;
    
    // 从纪元起的时间类型,用于为用户提供时间壁钟
    using time_t = /* 实现定义 */;
    
    // 日历时间类型
    struct tm;
    
    // 返回自程序启动时起的原始处理器时钟时间
    clock_t clock();
    
    // 计算时间之间的差,返回time1 - time0
    double difftime(time_t time1, time_t time0);
    
    // 转换本地日历时间为从纪元起的时间
    time_t mktime(struct tm* timeptr);
    
    // 返回当前日历时间,如果timer非空,也将它存储于timer中
    time_t time(time_t* timer);

    // 将日历时间类型转换成文本表示
    char* asctime(const struct tm* timeptr);
    
    // 将time_t对象转换成文本表示
    char* ctime(const time_t* timer);
    
    // 转换纪元起时间为以协调世界时表示的日历时间
    struct tm* gmtime(const time_t* timer);
    
    // 转换纪元起时间为以本地时间表示的日历时间
    struct tm* localtime(const time_t* timer);
    
    // 转换tm对象到自定义的文本表示
    size_t strftime(char* s, size_t maxsize, const char* format, const struct tm* timeptr);
}

§clock_t

clock_t是函数clock返回的类型,用于保存进程从关联到程序执行的实现定义时期开始,所用的粗略处理器时间。为转换结果为秒,可将它除以CLOCKS_PER_SEC。clock返回的时间不一定是精确的进程运行时间,因此只有两个clock_t的差才是有意义的。例如下面使用clock计算代码执行时间:

1
2
3
4
5
std::clock_t start = std::clock();
// ...
std::clock_t end = std::clock();
std::cout << static_cast<double>(end - start) / CLOCKS_PER_SEC
          << " seconds" << std::endl;

§time_t

time_t类型表示自1970年1月1日以来经过的秒数,对时间和日期的处理一般通过time_t类型进行。c++中的system_clock提供了一个静态成员函数to_time_t用来将某个时间点转换成time_t类型,用from_time_t来将一个time_t类型转换成时间点类型,它们的声明为:

1
2
static std::time_t to_time_t(const time_point &t) noexcept;
static std::chrono::system_clock::time_point from_time_t(std::time_t t) noexcept;

time_t类型向用户可读的格式转换可以使用ctime函数来完成。ctime函数用于将time_t对象转换成一个字符串,这个字符串拥有这样的格式:

Www Mmm dd hh:mm:ss yyyy\n

其中

  • Www - 星期。Mon、Tue、Wed、Thu、Fri、Sat、Sun之一。
  • Mmm - 月份。Jan、Feb、Mar、Apr、May、Jun、Jul、Aug、Sep、Oct、Nov、Dec之一。
  • dd - 几号
  • hh - 时
  • mm - 分
  • ss - 秒
  • yyyy - 年

因此如果需要向用户输出当前时间,可以使用system_clock::now()来获得当前的时间,然后转换成time_t类型的对象,也可以直接由函数time返回一个time_t对象,然后使用acstime将它转换成文本后输出,也可以使用gmtime或者localtime来获得不同时区的时间结果,如:

1
2
3
4
5
6
7
std::time_t t = std::chrono::system_clock::to_time_t(
        std::chrono::system_clock::now()
);
std::time_t t = std::time(nullptr);  // 两种t的计算方法取其一即可
std::cout << std::ctime(&t);         // 直接输出
std::cout << "UTC: " << std::asctime(std::gmtime(&t));        // 世界协调时
std::cout << "local: " << std::asctime(std::localtime(&t));   // 本地时间

结果为:

Thu Aug 20 16:35:10 2020
UTC: Thu Aug 20 08:35:10 2020
local: Thu Aug 20 16:35:10 2020

注意:gmtime和localtime返回的指针指向一个内部静态变量,因此返回后必须马上使用,或者拷贝到一个新的tm对象内,否则下一次调用后就被覆盖了。比如

1
2
3
4
5
std::time_t t = std::time(nullptr);
std::tm *t1 = std::gmtime(&t);
std::tm *t2 = std::localtime(&t);
std::cout << "UTC: " << std::asctime(t1);
std::cout << "local: " << std::asctime(t2);

可能的输出:

Thu Aug 20 16:33:06 2020
Thu Aug 20 16:33:06 2020

两者输出的都是本地时间!

§tm

tm是表示日期和时间的组合的结构体,定义为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct tm {
    int tm_sec;    // 分后之秒 - [0, 60]
    int tm_min;    // 时后之分 – [0, 59]
    int tm_hour;   // 自午夜起之时 – [0, 23]
    int tm_mday;   // 月之日 – [1, 31]
    int tm_mon;    // 自一月起之月 – [0, 11]
    int tm_year;   // 自 1900 起之年
    int tm_wday;   // 自星期日起之日 – [0, 6]
    int tm_yday;   // 自1月1日起之日 – [0, 365]
    int tm_isdst;  // 夏令时标志。值若夏令时有效则为正,若无效则为零,若无可用信息则为负
};

tm使我们可以比较方便地对日期和时间进行修改,而不是只从时钟获取时间。同时,标准库中提供了一组函数用于对时间和日期的格式进行定制:

1
2
3
4
5
6
7
8
#include <ctime>
char* asctime(const std::tm* time_ptr);
std::size_t strftime(char* str, std::size_t count, const char* format,
                     const std::tm* time);

#include <iomanip>
template< class CharT >
/*unspecified*/ put_time(const std::tm* tmb, const CharT* format);

asctime之于tm相当于ctime之于time_t,它们都返回一个指向静态空终止字符串的指针,这个字符串可能在asctime和ctime内共享。这两个函数都不是线程安全的,posix标准将它们标记为过时的,应当使用strftime来代替。

strftime函数将按照字符串format描述的格式,将tm对象转换成空终止字符串,放入str所指向的内存,最多写入count个字节。put_time将tm对象按照format所描述的格式输出。

格式化字符串format由普通字符串构成,其中含有像%Y这样的格式化字符串的将会按下表处理:

转换 指定符 解释 使用的域
% 写字面的%。完整转换指定必须是%%。
n 写换行符
t 写水平制表符
Y 以 4 位十进制数写 tm_year
EY 以替用方式写。例如在 ja_JP 本地环境中,以“平成23年”取代“2011年”。 tm_year
y 的末 2 位十进制数(范围[00, 9])。 tm_year
Oy 以替用数字系统写的末 2 位数字。例如在 ja_JP 本地环境中以“十一”取代“11”。 tm_year
Ey 写作从本地环境的替用时期 %EC 的偏移(本地环境依赖)。 tm_year
C 的首 2 位十进制数(范围[00, 99])。 tm_year
EC 以本地环境的替用表示写年份基底(时期),例如 ja_JP 中的“平成”。 tm_year
G 写基于ISO 8601的,即是包含指定星期的年份。IS0 8601中星期以星期一开始,而且一年的首星期必须满足下列要求:包含 1 月 4 日包含一年的首个星期四 tm_yea, tm_wday, tm_yday
g 写基于ISO 8601的,即是包含指定星期年份,的后 2 位数(范围[00, 99])。IS0 8601 中星期以星期一开始,而且一年的首星期必须满足下列要求:包含 1 月 4 日包含一年的首个星期四 tm_year, tm_wday, tm_yday
b 缩略月名,例如Oct(本地环境依赖)。 tm_mon
h 与b同意。 tm_mon
B 完整月名,例如October(本地环境依赖)。 tm_mon
m 写作十进制数(范围[01,12])。 tm_mon
Om 以替用数字系统写。例如 ja_JP 本地环境中“十二”取代“12”。 tm_mon
星期
U 以十进制数写年的星期(星期日是星期的首日)(范围[00, 53])。 tm_year, tm_wday, tm_yday
OU 以替用数值系统写如同用%U的年的星期。例如“五十二”在 ja_JP 本地环境中取代“52”。 tm_year, tm_wday, tm_yday
W 以十进制数写年的星期(星期一是星期的首日)(范围[00, 53])。 tm_year, tm_wday, tm_yday
OW 以替用数值系统写如同用%W的年的第几星期。例如“五十二”在 ja_JP 本地环境中取代“52”。 tm_year, tm_wday, tm_yday
V ISO 8601的年的星期(范围[00, 53])。IS0 8601中星期以星期一开始,而且一年的首星期必须满足下列要求:包含 1 月 4 日包含一年的首个星期四 tm_year, tm_wday, tm_yday
OV 以替用数值系统写如同用%V的年的星期。例如“五十二”在 ja_JP 本地环境中取代“52”。 tm_year, tm_wday, tm_yday
j 以十进制数写年的第几日(范围[001, 366])。 tm_yday
d 以十进制数写月的第几日(范围[01, 31])。 tm_mday
Od 以替用数字系统写零基的月的第几日。例如 ja_JP 本地环境中“二十七”取代“ 27 ”。单字符前加空格。 tm_mday
e 以十进制数写月的第几日(范围[01, 31])。单数字前加空格。 tm_mday
Oe 以替用数字系统写一基的月的第几日。例如 ja_JP 本地环境中“二十七”取代“27”。单字符前加空格。 tm_mday
星期几
a 缩略的星期日期名,例如 Fri (本地环境依赖)。 tm_wday
A 完整的星期日期名,例如 Friday (本地环境依赖)。 tm_wday
w 以十进制数写星期日期,其中星期日是 0 (范围[0, 6])。 tm_wday
Ow 用替用数字系统写星期日期,其中星期日是 0 。例如 ja_JP 本地环境中“二”取代“2”。 tm_wday
u 十进制数写星期日期,其中星期一是 1(ISO 8601格式)(范围[1, 7])。 tm_wday
Ou 用替用数字系统写星期日期,其中星期一是 1 。例如 ja_JP 本地环境中“二”取代“2”。 tm_wday
时、分、秒
H 以十进制数写, 24 小时制(范围[00, 23])。 tm_hour
OH 以替用数字系统写 24 小时制的。例如 ja_JP 本地环境中“十八”取代“18”。 tm_hour
I 以十进制数写, 12 小时制(范围[01, 12])。 tm_hour
OI 以替用数字系统写 12 小时制的。例如 ja_JP 本地环境中“六”取代“6”。 tm_hour
M 以十进制数写(范围[00, 59])。 tm_min
OM 以替用数字系统写。例如 ja_JP 本地环境中“二十五”取代“25”。 tm_min
S 以十进制数写(范围[00, 60])。 tm_sec
OS 以替用数字系统写。例如 ja_JP 本地环境中“二十四”取代“24”。 tm_sec
其他
c 标准日期时间字符串。例如 Sun Oct 17 04:41:13 2010 (本地环境依赖)。 全部
Ec 替用日期时间字符串。例如 ja_JP 本地环境中“平成23年”取代“2011年”。 全部
x 写本地化的日期表示(本地环境依赖)。 全部
Ex 替用日期表示。例如 ja_JP 本地环境中“平成23年”取代“2011年”。 全部
X 写本地化的时间表示(本地环境依赖)。 全部
EX 替用时间表示(本地环境依赖)。 全部
D 等价于**"%m/%d/%y"**。 tm_mon, tm_mday, tm_year
F 等价于**"%Y-%m-%d"**(ISO 8601日期格式)。 tm_mon, tm_mday, tm_year
r 写本地化的 12小时制时间(本地环境依赖)。 tm_hour, tm_min, tm_sec
R 等价于"%H:%M"。 tm_hour, tm_min
T 等价于"%H:%M:%S"( ISO 8601 时间格式)。 tm_hour, tm_min, tm_sec
p 写本地化的 a.m. 或 p.m. (本地环境依赖)。 tm_hour
z 以 ISO 8601 格式(例如-0430)写距UTC的偏移,或者倘若时区信息不可用则不写字符。 tm_isdst
Z 写依赖本地环境的时区名或缩写,或者若时区信息不可用则不写字符。 tm_isdst

例如以中文输出当前日期和时间:

1
2
3
std::time_t t = std::time(nullptr);
std::cout.imbue(std::locale{"zh_CN.utf8"});
std::cout << std::put_time(std::localtime(&t), "%c");

也可以使用strftime将它保存到字符串中:

1
2
3
4
std::locale::global(std::locale{"zh_CN.utf8"});
char str[100];
std::strftime(str, sizeof(str), "%x %X", std::localtime(&t));
std::cout << str << std::endl;

它们的结果为:

2020年08月20日 星期四 21时32分29秒
2020年08月20日 21时32分29秒

需要注意,strftime的本地环境由std::locale::global指定,而put_time在输出时必须使用out << put_time的形式将out绑定的本地环境传递给put_time的结果(put_time的返回值是未定义的,可以返回一个迭代器,使用cout输出时将结果转换成本地环境结果)。

§参考

[1] https://zh.cppreference.com/w/cpp/io/manip/put_time.

[2] https://zh.cppreference.com/w/cpp/chrono/c/strftime.

[3] https://zh.cppreference.com/w/cpp/header/ctime.

有关协调世界时、闰秒等信息可以参考以下链接:

[4] 世界协调时(UTC).

[5] 格林尼治标准时间(GMT).

[6] 国际原子时

[7] 闰秒.

加载评论