ZetCode

C 日期时间

最后修改日期:2023年1月18日

在本教程中,我们将介绍如何在 C 语言中处理日期和时间。在本教程中,我们使用 C99 标准。如果您在 Windows 上编译程序,我们强烈推荐 Pelles C 编译器。Windows API 教程有一个专门介绍 日期和时间 的章节。

C 日期时间定义

我们从一些定义开始。日历时间,也称为绝对时间,是时间连续体中的一个点,例如 2016 年 2 月 17 日 13:02:5 CET。时间间隔 是两个日历时间之间时间连续体的一个连续部分,例如 2000 年 2 月 20 日 13:00 和 14:00 之间的一小时。经过时间 是间隔的长度,例如 28 分钟。

时间量 是经过时间的总和。经过的时间不必是连续的。当工作花费了我们十一个小时时,我们可能在不同的日子里工作。周期 是两个事件之间间隔的经过时间。CPU 时间 是中央处理单元 (CPU) 用于处理计算机程序或操作系统指令的时间量。它以时钟滴答或秒为单位进行测量。

纪元 是选择为某个时代起点的时刻。Unix 纪元 是 1970 年 1 月 1 日 00:00:00 UTC 的时间(或 ISO 8601 的 1970-01-01T00:00:00Z)。

简单时间 是日历时间的紧凑表示;它是自 Unix 纪元以来的经过时间的秒数。简单时间使用 time_t 数据类型。分解时间 表示人类可读的日历时间。它被分解为一组指定公历中年份、月份、日期等信息的组件,针对特定时区。分解时间使用 struct tm 数据类型。

挂钟时间,也称为真实世界时间或挂钟时间,指的是由计时器(如手表或挂钟)确定的经过时间。(提及挂钟是该术语最初名称的由来。)挂钟时间不同于通过计算微处理器时钟脉冲或周期来测量的时间。

time.h 包含以下函数原型

此外,time.h 文件还定义了 CLOCKS_PER_SEC 宏,它包含每秒的处理器时钟滴答数。clock_t 是进程运行时间类型。

Unix 时间

Unix 时间是自 Unix 纪元以来的秒数。time 函数返回自 1970 年 1 月 1 日协调世界时 0 时 0 分 0 秒以来的时间(以秒为单位)。如果发生错误,它会返回 -1。

time_t time(time_t *t);

如果 t 不是 NULL,返回值还将存储在 t 所指向的内存中。

unixtime.c
#include <stdio.h>
#include <time.h>

int main(void) {

    time_t now = time(NULL);

    if (now == -1) {

        puts("The time() function failed");
    }

    printf("%ld\n", now);

    return 0;
}

该示例打印 Unix 时间。

$ ./unixtime
1674029496

此刻,自 Unix 纪元以来已经过去了 1674029496 秒。

分解时间

分解时间是日历时间的人类可读版本。struct tm 数据类型用于分解时间。localtime 函数将简单日历时间转换为分解时间。它会考虑当前时区。

struct tm *localtime(const time_t *clock);

它存储一个 tm 结构并返回指向该结构的指针。

以下 struct tm 的描述取自 FreeBSD 手册。

struct tm {
    int tm_sec;         /* seconds */
    int tm_min;         /* minutes */
    int tm_hour;        /* hours */
    int tm_mday;        /* day of the month */
    int tm_mon;         /* month */
    int tm_year;        /* year */
    int tm_wday;        /* day of the week */
    int tm_yday;        /* day in the year */
    int tm_isdst;       /* daylight saving time */
};

tm 结构体的成员是

brokendowntime.c
#include <stdio.h>
#include <time.h>

int main(void) {

    time_t rawtime = time(NULL);

    if (rawtime == -1) {

        puts("The time() function failed");
        return 1;
    }

    struct tm *ptm = localtime(&rawtime);

    if (ptm == NULL) {

        puts("The localtime() function failed");
        return 1;
    }

    printf("The time is: %02d:%02d:%02d\n", ptm->tm_hour,
           ptm->tm_min, ptm->tm_sec);

    return 0;
}

在该示例中,我们获取 Unix 时间并将其转换为本地分解时间。

time_t rawtime = time(NULL);

我们获取原始 Unix 时间。

struct tm *ptm = localtime(&rawtime);

以秒为单位表示的原始时间被转换为分解时间。该函数填充 struct tm 类型并返回指向它的指针。该结构是静态分配的,因此我们不必释放它。

printf("The time is: %02d:%02d:%02d\n", ptm->tm_hour,
        ptm->tm_min, ptm->tm_sec);

我们使用 tm_hourtm_mintm_sec 成员来表示人类可读的时间格式。

$ ./brokendowntime
The time is: 17:00:12

将分解时间转换为 Unix 时间

mktime 函数将 tm 指向的结构中表示的本地时间的分解时间转换为 Unix 时间(简单日历时间)。

time_t mktime(struct tm *tm);

mktime 函数接收指向 struct tm 值的指针并返回 time_t 值。

xmas.c
#include <stdio.h>
#include <time.h>

int main(void) {

    struct tm xmas = { 0, 0, 0, 24, 11, 116 };

    time_t rawtime = mktime(&xmas);

    if (rawtime == -1) {

         puts("The mktime() function failed");
         return 1;
    }

    printf("The Unix time for Xmas is: %ld\n", rawtime);

    return 0;
}

在该示例中,我们计算未来某个时间的 Unix 时间。

struct tm xmas = { 0, 0, 0, 24, 11, 116 };

这是 2016 年圣诞节开始时间的时间值。

time_t rawtime = mktime(&xmas);

使用 mktime 函数,我们将分解时间转换为 Unix 时间。

$ ./xmas
The Unix time for Xmas is: 1482534000

1482534000 是 2016 年圣诞节开始时间的 Unix 时间。

UTC 时间

我们的星球是一个球体;它围绕其轴线旋转。地球向东旋转,因此太阳在不同地点以不同的时间升起。地球大约每 24 小时旋转一次。因此,世界被划分为 24 个时区。每个时区都有不同的本地时间。此本地时间通常会进一步受到夏令时的影响。

有实际需要一种全球统一的时间。一种全球统一的时间有助于避免在时区和夏令时方面的混淆。UTC(协调世界时)被选为主要时间标准。UTC 用于航空、天气预报、飞行计划、空中交通管制许可和地图。与本地时间不同,UTC 随季节变化而不改变。

gmtimetime_t 值转换为分解时间,而不进行时区调整。

utctime.c
#include <stdio.h>
#include <time.h>

int main(void) {

    time_t now = time(&now);

    if (now == -1) {

        puts("The time() function failed");
    }

    struct tm *ptm = gmtime(&now);

    if (ptm == NULL) {

        puts("The gmtime() function failed");
    }

    printf("UTC time: %s", asctime(ptm));

    return 0;
}

该示例打印当前的 UTC 时间。

struct tm *ptm = gmtime(&now);

我们使用 gmtime 函数获取 UTC 时间。

printf("UTC time: %s", asctime(ptm));

我们打印 UTC 时间。asctime 函数已弃用,我们应该改用 strftime,它将在后面介绍。

$ ./utctime 
UTC time: Wed Jan 18 08:34:29 2023
$ date
St 18. január 2023, 09:34:31 CET

对于 CET 时区,时间存在一个小时的差异。

格式化时间

strftime 函数格式化日期和时间。

size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

strftime 函数根据格式说明符 format 格式化分解时间 tm,并将结果放置在大小为 max 的字符数组 s 中。

日期和时间格式说明符列表
说明符 含义 示例
%% % 字符。 %
%a 国家缩写星期名称 Mon
%A 国家全称星期名称 Monday
%b 国家缩写月份名称 Sep
%B 国家全称月份名称 September
%c 日期和时间的国家表示 Mon Spe 22 12:50:32 2011
%C 世纪数 (00-99) 19
%d 月份中的日期,零填充 (01-31) 22
%D 短 MM/DD/YY 日期,等同于 %m/%d/%y 07/30/09
%e 月份中的日期,空格填充 ( 1-31) 22
%F 短 YYYY-MM-DD 日期,等同于 %Y-%m-%d 2011-09-22
%g 基于周的年份,后两位数字 (00-99) 16
%G 基于周的年份,带世纪。 2016
%h 与 %b 相同。 Sep
%H 24 小时制的小时 (00-23) 16
%I 12 小时制的小时 (01-12) 08
%j 一年中的第几天 (001-366) 145
%m 月份的十进制数字 (01-12) 08
%M 分钟 (00-59) 52
%n 换行符 ('\n')
%p AM 或 PM 标志 AM
%r 12 小时制时钟时间 02:55:02 PM
%R 24 小时制 HH:MM 时间,等同于 %H:%M 12:44
%S 秒 (00-61) 06
%s Unix 时间;自 Unix 纪元以来的秒数。 1455803239
%t 水平制表符 ('\t')
%T ISO 8601 时间格式 (HH:MM:SS),等同于 %H:%M:%S 18:25:34
%u ISO 8601 星期几,数字表示,星期一为 1 (1-7) 6
%U 星期数,以第一个星期日为第一周的第一天 (00-53) 30
%V ISO 8601 星期数 (00-53) 12
%w 星期几的十进制数字表示,星期日为 0 (0-6) 5
%W 星期数,以第一个星期一为第一周的第一天 (00-53) 50
%x 国家的日期表示 05/28/11
%X 国家的日期和时间表示 12:22:02
%y 年份,后两位数字 (00-99) 11
%Y 年份 2016
%z 与 UTC 的时区偏移;前导加号表示东于 UTC,减号表示西于 UTC,小时和分钟各跟两位数字且中间无分隔符。如果无法确定时区,则不显示任何字符。 +0100
%Z 时区名称或缩写;如果无法确定时区,则不显示任何字符 CEST

以下示例展示了一些格式说明符。

today.c
#include <stdio.h>
#include <time.h>
#include <string.h>

#define BUF_LEN 256

int main(void) {

    char buf[BUF_LEN] = {0};

    time_t rawtime = time(NULL);

    if (rawtime == -1) {

        puts("The time() function failed");
        return 1;
    }

    struct tm *ptm = localtime(&rawtime);

    if (ptm == NULL) {

        puts("The localtime() function failed");
        return 1;
    }

    strftime(buf, BUF_LEN, "%d/%m/%Y", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "%d. %m. %Y", ptm);

    puts(buf);

    return 0;
}

该示例以英国和斯洛伐克格式打印今天的日期。

strftime(buf, BUF_LEN, "%d/%m/%Y", ptm);

这行代码以英国格式创建日期字符串。我们使用 %d%m%Y 格式说明符。

strftime(buf, BUF_LEN, "%d. %m. %Y", ptm);

在这行中,创建了斯洛伐克日期格式。

$ ./today
18/01/2023
18. 01. 2023

在以下示例中,我们将使用其他格式说明符。

time_formats.c
#include <stdio.h>
#include <time.h>
#include <string.h>

#define BUF_LEN 256

int main(void) {

    char buf[BUF_LEN] = {0};

    time_t rawtime = time(NULL);

    if (rawtime == -1) {

        puts("The time() function failed");
        return 1;
    }

    struct tm *ptm = localtime(&rawtime);

    if (ptm == NULL) {

        puts("The localtime() function failed");
        return 1;
    }

    strftime(buf, BUF_LEN, "Today is %A", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "The month is %B", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "Today is %-d day of %B", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "Today is %-j day of %G", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "Today is %-W week of %G", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "The time is %T", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "The date is %D", ptm);
    puts(buf);

    memset(buf, 0, BUF_LEN);
    strftime(buf, BUF_LEN, "The timezone is %Z", ptm);
    puts(buf);

    return 0;
}

在代码示例中,我们确定当前时间并通过 strftime 函数将其转换为额外信息。

strftime(buf, BUF_LEN, "Today is %A", ptm);

使用 %A 说明符,我们获得完整的星期名称。

strftime(buf, BUF_LEN, "The month is %B", ptm);

使用 %B 说明符,我们获得完整的月份名称。

strftime(buf, BUF_LEN, "Today is %-d day of %B", ptm);

使用 %d 说明符,我们获得月份中的日期。% 后面的 - 字符会移除值的前导零。

strftime(buf, BUF_LEN, "Today is %-j day of %G", ptm);

%j 说明符给出了一年中的第几天;%G 说明符给出了一年。

strftime(buf, BUF_LEN, "Today is %-W week of %G", ptm);

%W 说明符给出了一年的星期数。

strftime(buf, BUF_LEN, "The time is %T", ptm);

%T 说明符给出 ISO 8601 时间格式。

strftime(buf, BUF_LEN, "The date is %D", ptm);

%D 说明符给出短日期格式。

strftime(buf, BUF_LEN, "The timezone is %Z", ptm);

最后,ZT 说明符给出时区名称。

$ ./time_formats 
Today is Wednesday
The month is January
Today is 18 day of January
Today is 18 day of 2023
Today is 3 week of 2023
The time is 09:36:24
The date is 01/18/23
The timezone is CET

在 2023 年 1 月 18 日,我们得到这些结果。

在以下示例中,我们使用本地化的日期格式。

localized.c
#include <stdio.h>
#include <time.h>
#include <locale.h>

#define BUF_LEN 256

int main(void) {

    char buf[BUF_LEN] = {0};

    if (setlocale(LC_TIME, "sk_SK.utf8") == NULL) {

        puts("Unable to set locale");
        return 1;
    }

    time_t now = time(NULL);

    if (now == -1) {

        puts("The time() function failed");
        return 1;
    }

    struct tm *ptm = localtime(&now);

    if (ptm == NULL) {

        puts("The localtime() function failed");
        return 1;
    }

    strftime(buf, BUF_LEN, "%c", ptm);
    printf("Dnešný dátum: %s\n", buf);

    return 0;
}

该示例以斯洛伐克语打印今天的日期。

if (setlocale(LC_TIME, "sk_SK.utf8") == NULL) {

    puts("Unable to set locale");
    return 1;
}

使用 setlocale 函数,我们设置斯洛伐克语环境。

strftime(buf, BUF_LEN, "%c", timeinfo);

我们使用 %c 说明符,它表示日期和时间的国家表示。

$ ./localized 
Dnešný dátum: St 18. január 2023, 09:37:55

在 2023 年 1 月 18 日,我们得到以斯洛伐克语显示的这个斯洛伐克日期格式。

解析时间

strptime 函数用于将时间的字符串表示转换为 tm 结构(分解时间)。strptime 函数是 strftime 函数的逆函数。

char *strptime(const char *s, const char *format, struct tm *tm);

该函数的第一参数是以字符串形式表示的时间。第二参数是字符串格式。第三个参数是指向 struct tm 的指针。该函数返回指向在此函数调用中未处理的第一个字符的指针。失败时,该函数返回 NULL

parsetime.c
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <time.h>

int main(void) {

    struct tm tm = {0};

    char *s = strptime("16 Feb 2016 14:43:00", "%d %b %Y %H:%M:%S", &tm);

    if (s == NULL) {

        printf("Cannot parse date");
        return 1;
    }

    printf("year: %d; month: %d; day: %d;\n",
            tm.tm_year + 1900, tm.tm_mon, tm.tm_mday);
    printf("hour: %d; minute: %d; second: %d\n",
            tm.tm_hour, tm.tm_min, tm.tm_sec);
    printf("week day: %d; year day: %d\n", tm.tm_wday, tm.tm_yday);

    return 0;
}

该示例解析字符串时间并填充 struct tm;然后打印该结构的成员。

#define _XOPEN_SOURCE 700

此定义消除了 warning: initialization makes pointer from integer without a cast 警告。它告诉编译器包含一些在 X/Open 和 POSIX 标准中定义的额外函数的定义。

char *s = strptime("16 Feb 2016 14:43:00", "%d %b %Y %H:%M:%S", &tm);

我们使用 strptime 函数解析日期。格式说明符必须与字符串部分匹配。

printf("year: %d; month: %d; day: %d;\n",
        tm.tm_year + 1900, tm.tm_mon, tm.tm_mday);

在这里,我们打印 strptime 填充的 struct tmtm_yeartm_montm_mday 成员。

$ ./parsetime
year: 2016; month: 1; day: 16;
hour: 14; minute: 43; second: 0
week day: 2; year day: 46

这是 parsetime 示例的输出。

时间算术

time_t 类型是算术类型,我们可以对其进行算术运算。

时间函数处理的是 1900 年以后的日期。对于更早的日期,我们需要参考儒略日。

time_arithmetic.c
#include <stdio.h>
#include <time.h>

#define BUF_LEN 256

int main(void) {

    char buf[BUF_LEN] = {0};
    struct tm day = { 0, 0, 0, 20, 1, 116 };

    time_t one_day = 86400;
    time_t sixty_days = one_day * 60;

    time_t t1 = mktime(&day);

    if (t1 == -1) {

        puts("The mktime() function failed");
        return 1;
    }

    time_t t2 = t1 + one_day + sixty_days;

    struct tm *ptm = gmtime(&t2);

    if (ptm == NULL) {

        puts("The gmtime() function failed");
    }

    strftime(buf, BUF_LEN, "%F", ptm);
    puts(buf);

    return 0;
}

在该示例中,我们将 2016 年 2 月 20 日加上六十天,并得到结果日期。

struct tm day = { 0, 0, 0, 20, 1, 116 };

我们定义了初始日期;它是 2016 年 2 月 20 日。

time_t one_day = 86400;
time_t sixty_days = one_day * 60;

这是以秒为单位表示的六十天。

time_t t1 = mktime(&day);

mktime 方法将日期转换为秒。

time_t t2 = t1 + one_day + sixty_days;

我们通过以秒为单位加一天来转到一天的结束,然后再加上六十天。

struct tm *ptm = gmtime(&t2);

我们使用 gmtime 方法获取分解时间。

strftime(buf, BUF_LEN, "%F", ptm);
puts(buf);

我们使用 strftime 方法格式化日期,并使用 puts 方法打印它。

$ ./time_arithmetic
2016-04-20

在 2016 年 2 月 20 日加上 60 天得到 2016 年 4 月 20 日。

儒略日

儒略日 是自儒略周期开始以来的天数。它主要由天文学家使用。(不应与儒略历混淆。)儒略周期始于公元前 4713 年。儒略日编号 0 分配给开始于公元前 4713 年 1 月 1 日中午的那一天。

儒略日期 (JD) 是自该周期开始以来经过的天数。任何时刻的儒略日期是前一个中午的儒略日编号加上自该时刻以来的日分数。除了天文学,儒略日期也经常被军事和大型机程序使用。

days_since_borodino.c
#include <stdio.h>
#include <time.h>

#define BUF_LEN 256

int getJulianDay(int day, int month, int year);

int main(void) {

    time_t now = time(NULL);

    if (now == -1) {

        puts("The time() function failed");
        return 1;
    }

    struct tm *ptm = localtime(&now);

    if (ptm == NULL) {

        puts("The localtime() function failed");
        return 1;
    }

    int year = ptm->tm_year + 1900;
    int month = ptm->tm_mon + 1;
    int day = ptm->tm_mday;

    int jtoday = getJulianDay(day, month, year);
    int jborodino = getJulianDay(7, 9, 1812);

    int delta = jtoday - jborodino;

    printf("%d days have passed since the Borodino battle\n", delta);

    return 0;
}


int getJulianDay(int day, int month, int year) {

    int a = (14 - month) / 12;
    int y = year + 4800 - a;
    int m = month + 12*a - 3;

    int jdn = 0;

    if (year == 1582)
    {
        if (month == 10)
        {
            if (day > 4 && day < 15)
                day = 15;
        }
    }

    if ((year > 1582) || (year == 1582 && month > 10) ||
        (year == 1582 && month == 10 && day > 14))
    {
        jdn = day + (153*m+2)/5 + 365*y + y/4 - y/100 + y/400 - 32045;
    }
    else
    {
        jdn = day + (153*m+2)/5 + 365*y + y/4 - 32083;
    }

    return jdn;
}

在该示例中,我们计算自博罗季诺战役以来经过的天数。博罗季诺战役始于 1812 年 9 月 7 日。

time_t now = time(NULL);

if (now == -1) {

    puts("The time() function failed");
    return 1;
}

struct tm *ptm = localtime(&now);

if (ptm == NULL) {

    puts("The localtime() function failed");
    return 1;
}

int year = ptm->tm_year + 1900;
int month = ptm->tm_mon + 1;
int day = ptm->tm_mday;

我们确定今天的年、月、日。

int jtoday = getJulianDay(day, month, year);
int jborodino = getJulianDay(7, 9, 1812);

我们计算今天的儒略日和博罗季诺战役的儒略日。

int delta = jtoday - jborodino;

printf("%d days have passed since Borodino battle\n", delta);

我们计算天数差并将其打印到控制台。

int getJulianDay(int day, int month, int year) {

    int a = (14 - month) / 12;
    int y = year + 4800 - a;
    int m = month + 12*a - 3;

    int jdn = 0;

    if (year == 1582)
    {
        if (month == 10)
        {
            if (day > 4 && day < 15)
                day = 15;
        }
    }

    if ((year > 1582) || (year == 1582 && month > 10) ||
        (year == 1582 && month == 10 && day > 14))
    {
        jdn = day + (153*m+2)/5 + 365*y + y/4 - y/100 + y/400 - 32045;
    }
    else
    {
        jdn = day + (153*m+2)/5 + 365*y + y/4 - 32083;
    }

    return jdn;
}

getJulianDay 函数包含计算儒略日的算法。有两种计算方法;一种用于公历时代,一种用于前公历时代。

$ ./days_since_borodino
76834 days have passed since the Borodino battle

在 2023 年 1 月 18 日,自博罗季诺战役以来已过去 76834 天。

时区

时区 是使用相同标准时间的一个区域。世界上有 24 个时区。

UTC = local time + bias

偏差是 UTC 时间和本地时间之间的差值。

tzset 函数使用标准时区名称对初始化 tzname 数组:标准时区和夏令时时区。如果未使用夏令时时区,则第二个字符串为空。该函数还初始化 timezone 变量,该变量包含 UTC 与最新本地标准时间之间的差异(以西于 UTC 的秒数为单位)。

timezone.c
#include <stdio.h>
#include <time.h>

int main(void) {

    tzset();

    printf("The timezone is %s and %s\n", tzname[0], tzname[1]);

    printf("The bias is %ld seconds\n", timezone);

    return 0;
}

该程序打印当前时区和偏差。

tzset();

时区信息通过 tzset 函数设置。

printf("The timezone is %s and %s\n", tzname[0], tzname[1]);

tzset 函数用时区字符串填充 tzname 数组。

$ ./timezone
The timezone is CET and CEST
The bias is -3600 seconds

在一个位于布拉迪斯拉发的系统上,我们得到这些值。

时区信息也可以从 strftime 函数中检索。

timezone2.c
#include <stdio.h>
#include <time.h>

#define BUF_LEN 256

int main(void) {

    char buf[BUF_LEN] = {0};

    time_t rawtime = time(NULL);

    if (rawtime == -1) {

        puts("The time() function failed");
        return 1;
    }

    struct tm *ptm = localtime(&rawtime);

    if (ptm == NULL) {

        puts("The localtime function failed");
        return 1;
    }

    strftime(buf, BUF_LEN, "Timezone: %Z  bias: %z", ptm);
    puts(buf);

    return 0;
}

在该示例中,我们计算简单的日历值,将其分解为 tm 结构,并通过 strftime 函数获取时区信息。

strftime(buf, BUF_LEN, "Timezone: %Z  bias: %z", ptm);

%Z 说明符获取当前时区名称,%z 返回偏差。偏差以小时和分钟表示,每个后跟两位数字且中间无分隔符。前导加号表示东于 UTC。

$ ./timezone2
Timezone: CET  bias: +0100

系统上的时区是 CET。偏差是东于 UTC 的一小时。

闰年

闰年 是包含额外一天的年份。日历中多一天的原因是天文年和日历年之间的差异。

leapyear.c
#include <stdio.h>
#include <stdbool.h>

bool isLeapYear(int);

int main(void) {

    // Assume year >= 1582 in the Gregorian calendar.
    int years[] = { 2000, 2002, 2004, 2008, 2012, 2016, 2020,
        1900, 1800, 1600 };

    int size = sizeof(years) / sizeof(int);

    for (int i=0; i<size; i++) {

        if (isLeapYear(years[i])) {

            printf("%d is a leap year\n", years[i]);
        } else {

            printf("%d is not a leap year\n", years[i]);
        }
    }

    return 0;
}

bool isLeapYear(int year) {

    if (year % 4 != 0) {

        return false;
    } else if (year % 400 == 0) {

        return true;
    } else if (year % 100 == 0) {

        return false;
    } else {

        return true;
    }
}

我们有一个年份数组。我们检查所有年份是否为闰年。没有内置函数可以检查闰年;因此,创建了一个自定义的 isLeapYear 函数。

// Assume year >= 1582 in the Gregorian calendar.
int years[] = { 2000, 2002, 2004, 2008, 2012, 2016, 2020,
    1900, 1800, 1600 };

这是一个我们检查的年份数组。年份必须是公历。

for (int i=0; i<size; i++) {

    if (isLeapYear(years[i])) {

        printf("%d is a leap year\n", years[i]);
    } else {

        printf("%d is not a leap year\n", years[i]);
    }
}

通过 for 循环,我们遍历数组。我们使用 isLeapYear 函数检查一个年份是否是闰年。

bool isLeapYear(int year) {

    if (year % 4 != 0) {

        return false;
    } else if (year % 400 == 0) {

        return true;
    } else if (year % 100 == 0) {

        return false;
    } else {

        return true;
    }
}

这是确定闰年的函数。闰年是 4 的整数倍。是 100 的整数倍的年份不是闰年,除非它也是 400 的整数倍,在这种情况下它也是闰年。

$ ./leapyear
2000 is a leap year
2002 is not a leap year
2004 is a leap year
2008 is a leap year
2012 is a leap year
2016 is a leap year
2020 is a leap year
1900 is not a leap year
1800 is not a leap year
1600 is a leap year

这是 leapyear 程序的输出。

CPU 时间

CPU 时间是中央处理单元用于处理计算机程序指令的时间量。

clock_t clock(void);

clock 函数返回程序消耗的处理器时间。clock 函数的分辨率较低,并且在不同平台上不一致;因此,在需要高精度的场合不应使用它。

请注意,在 Windows 上,clock 函数计算调用进程使用的挂钟时间。挂钟时间 是根据计算机内部时钟经过的时间,它应该与外部世界的时间相匹配。

cputime.c
#include <stdio.h>
#include <time.h>

unsigned long long factorial(int);

int main(void) {

    clock_t start = clock();

    if (start == -1) {

        puts("The clock() function failed");
        return 1;
    }

    unsigned long long f = factorial(20);

    printf("The factorial of 20 is: %llu\n", f);

    clock_t end = clock();

    if (end == -1) {

        puts("The clock() function failed");
        return 1;
    }

    clock_t delta  = end - start;

    printf("The program took %lu clicks (%lf seconds).\n", delta,
           (double) delta/CLOCKS_PER_SEC);

    return 0;
}

unsigned long long factorial(int v) {

    if (v == 0) {

        return 1;
    } else {

        return v * factorial(v - 1);
    }
}

该程序计算一个数字的阶乘;我们使用 clock 函数来获取计算时间。

clock_t start = clock();

if (start == -1) {

    puts("The clock() function failed");
    return 1;
}

unsigned long long f = factorial(20);

printf("The factorial of 20 is: %llu\n", f);

clock_t end = clock();

我们在两个 clock 调用之间计算阶乘。

clock_t delta  = end - start;

我们得到开始和结束值之间的差值。

printf("The program took %lu clicks (%lf seconds).\n", delta,
        (double) delta/CLOCKS_PER_SEC);

将 delta 值除以 CLOCKS_PER_SEC,我们得到近似的持续时间(以秒为单位)。

$ ./cputime
The factorial of 20 is: 2432902008176640000
The program took 41 clicks (0.000041 seconds).

这是程序的示例输出。

高分辨率 CPU 时间

clock_gettime 函数可用于获取高分辨率时间时钟。

int clock_gettime(clockid_t, struct timespec *)

该函数的第一参数指定时钟 ID;有几种类型的时钟

CLOCK_REALTIME 是一个系统范围的时钟,它测量真实(挂钟)时间。此调时钟会受到系统管理员或 adjtime 和 NTP 引起的系统时间不连续跳转的影响。CLOCK_MONOTONIC 表示自某个未指定的起始点以来的单调时间。此调时钟不受系统时间不连续跳转的影响。CLOCK_PROCESS_CPUTIME_ID 是 CPU 的高分辨率进程计时器。CLOCK_THREAD_CPUTIME_ID 是特定于线程的 CPU 时间时钟。

clock_gettime 函数的第二参数是 timespec 结构。

struct timespec {
    time_t   tv_sec;    /* seconds */
    long     tv_nsec;   /* nanoseconds */
};

timespec 结构包含秒和纳秒。

cputime.c
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <errno.h>

unsigned long long factorial(int);
double time_spec_seconds(struct timespec *);

int main(void) {

    struct timespec tstart = {0,0}, tend = {0,0};
    int r = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tstart);

    if (r == -1) {

        printf("The clock_gettime() function failed: %s\n", strerror(errno));
        return 1;
    }

    unsigned long long f = factorial(20);

    printf("The factorial of 20 is: %llu\n", f);

    r = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tend);

    if (r == -1) {

        printf("The clock_gettime() function failed: %s\n", strerror(errno));
        return 1;
    }

    double delta = time_spec_seconds(&tend) - time_spec_seconds(&tstart);

    printf("The computation took %lf seconds.\n", delta);

    return 0;
}

double time_spec_seconds(struct timespec* ts) {

    return (double) ts->tv_sec + (double) ts->tv_nsec * 1.0e-9;
}

unsigned long long factorial(int v) {

    if (v == 0) {

        return 1;
    } else {

        return v * factorial(v - 1);
    }
}

使用 clock_gettime,我们计算阶乘计算的 CPU 时间。

struct timespec tstart = {0,0}, tend = {0,0};

我们创建并初始化两个 timespec 结构。

int r = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tstart);

我们调用 clock_gettime 并使用 CLOCK_PROCESS_CPUTIME_ID

double delta = time_spec_seconds(&tend) - time_spec_seconds(&tstart);

通过减去两个 timespec 结构,我们得到 delta 值。

double time_spec_seconds(struct timespec* ts) {

    return (double) ts->tv_sec + (double) ts->tv_nsec * 1.0e-9;
}

time_spec_seconds 函数将 timespec 结构转换为秒。

$ ./cputime2
The factorial of 20 is: 2432902008176640000
The computation took 0.000081 seconds.

来源

撰写本文档使用了以下来源

在本文中,我们介绍了 C 语言中的日期和时间处理。