Dates and Times

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

<time.h> covers three separate concerns that are easy to confuse: a calendar time (a point in civil time), a wall-clock timestamp with sub-second resolution, and processor time consumed. Using the wrong one is how benchmarks end up measuring the wrong thing.

The Three Time Types

Type Obtained from Represents

time_t

time(nullptr)

A calendar time with (in practice) one-second resolution. Almost universally seconds since the Unix epoch, though C does not require that.

struct timespec

timespec_get

Seconds plus nanoseconds. C11’s higher-resolution timestamp.

clock_t

clock()

Processor time used by the program, in CLOCKS_PER_SEC units — not wall time.

struct tm

gmtime/localtime

A broken-down calendar time: year, month, day, hour, minute, second, and more.

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

int main(void)
{
    // 1. A calendar time, to the second.
    time_t now = time(nullptr);
    if (now == (time_t)-1) {
        fputs("time is unavailable\n", stderr);
        return 1;
    }

    // 2. A timestamp with nanosecond resolution (C11).
    struct timespec ts;
    if (timespec_get(&ts, TIME_UTC) != TIME_UTC) {
        fputs("timespec_get failed\n", stderr);
        return 1;
    }

    // 3. Processor time consumed so far.
    clock_t cpu = clock();

    printf("time_t          = %lld\n", (long long)now);
    printf("timespec        = %lld.%09ld\n", (long long)ts.tv_sec, ts.tv_nsec);
    printf("cpu seconds     = %g\n", (double)cpu / CLOCKS_PER_SEC);
    printf("CLOCKS_PER_SEC  = %ld\n", (long)CLOCKS_PER_SEC);
    return 0;
}

time_t is an arithmetic type of unspecified representation, so print it by casting to long long — there is no printf specifier for it. C23 adds timespec_getres for querying a clock’s resolution, and TIME_MONOTONIC and TIME_ACTIVE/TIME_THREAD_ACTIVE as optional bases alongside the mandatory TIME_UTC.

Broken-Down Time — struct tm

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

int main(void)
{
    time_t now = time(nullptr);

    // Two conversions: UTC or the local time zone.
    struct tm utc = *gmtime(&now);
    struct tm local = *localtime(&now);

    // The field conventions are the classic C gotcha:
    printf("UTC:   %04d-%02d-%02d %02d:%02d:%02d (yday %d, wday %d, isdst %d)\n",
           utc.tm_year + 1900,      // years SINCE 1900
           utc.tm_mon + 1,          // 0 = January
           utc.tm_mday,             // 1..31, this one is 1-based
           utc.tm_hour,             // 0..23
           utc.tm_min,              // 0..59
           utc.tm_sec,              // 0..60 (60 allows a leap second)
           utc.tm_yday,             // 0..365
           utc.tm_wday,             // 0 = Sunday
           utc.tm_isdst);           // >0 in DST, 0 not, <0 unknown

    printf("Local: %04d-%02d-%02d %02d:%02d:%02d\n",
           local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
           local.tm_hour, local.tm_min, local.tm_sec);
    return 0;
}

Note the gmtime(&now) copy. gmtime and localtime return a pointer to a *single static struct tm, so the next call overwrites it — copying immediately is the only safe use in any program with more than one conversion, and mandatory in threaded code. C23 standardizes the reentrant gmtime_r and localtime_r that POSIX has always had:

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

int main(void)
{
    time_t now = time(nullptr);
    struct tm buffer;

    // C23 (POSIX for decades): the caller supplies the storage.
    struct tm *utc = gmtime_r(&now, &buffer);
    if (utc == nullptr) {
        return 1;
    }

    printf("%04d-%02d-%02d\n", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
    return 0;
}

Constructing and Normalizing a Time

mktime goes the other way — broken-down local time to time_t — and normalizes out-of-range fields, which makes it C’s date arithmetic:

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

int main(void)
{
    // Build a specific local date: 2026-02-28 12:00:00
    struct tm date = {
        .tm_year = 2026 - 1900,
        .tm_mon = 2 - 1,
        .tm_mday = 28,
        .tm_hour = 12,
        .tm_min = 0,
        .tm_sec = 0,
        .tm_isdst = -1,             // -1: let mktime work out whether DST applies
    };

    time_t stamp = mktime(&date);   // note: mktime MODIFIES date, normalizing it
    if (stamp == (time_t)-1) {
        fputs("unrepresentable date\n", stderr);
        return 1;
    }

    printf("2026-02-28 -> %lld, weekday %d\n", (long long)stamp, date.tm_wday);

    // Date arithmetic by normalization: add 5 days by overflowing tm_mday.
    struct tm later = date;
    later.tm_mday += 5;
    later.tm_isdst = -1;

    if (mktime(&later) == (time_t)-1) {
        return 1;
    }
    printf("+5 days = %04d-%02d-%02d\n",
           later.tm_year + 1900, later.tm_mon + 1, later.tm_mday);   // 2026-03-05

    // C23: timegm is the UTC counterpart of mktime (POSIX had it long before).
    struct tm utc_date = {
        .tm_year = 2026 - 1900, .tm_mon = 0, .tm_mday = 1,
        .tm_hour = 0, .tm_min = 0, .tm_sec = 0,
    };
    time_t utc_stamp = timegm(&utc_date);
    printf("2026-01-01T00:00:00Z -> %lld\n", (long long)utc_stamp);
    return 0;
}

Adding "one day" as + 86400 seconds is wrong across a DST transition; normalizing tm_mday through mktime is right, because it re-resolves the offset. Note that mktime interprets its input as local time — always set tm_isdst = -1 unless you genuinely know the answer.

Formatting with strftime

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

int main(void)
{
    time_t now = time(nullptr);
    struct tm utc;

    if (gmtime_r(&now, &utc) == nullptr) {
        return 1;
    }

    char buffer[128];

    // strftime returns the number of bytes written, or 0 if it did not fit.
    if (strftime(buffer, sizeof buffer, "%Y-%m-%dT%H:%M:%SZ", &utc) == 0) {
        fputs("format did not fit\n", stderr);
        return 1;
    }
    printf("ISO 8601 : %s\n", buffer);

    strftime(buffer, sizeof buffer, "%A, %d %B %Y", &utc);
    printf("long     : %s\n", buffer);

    strftime(buffer, sizeof buffer, "%a %b %e %H:%M:%S %Y", &utc);
    printf("asctime  : %s\n", buffer);

    strftime(buffer, sizeof buffer, "week %V of %G, day %u", &utc);
    printf("ISO week : %s\n", buffer);
    return 0;
}

The specifiers worth knowing: %Y 4-digit year, %m month, %d day, %H/%M/%S time, %j day of year, %A/%a weekday name, %B/%b month name, %p AM/PM, %Z zone name, %z numeric offset, %V/%G ISO week and week-based year, %F (%Y-%m-%d), %T (%H:%M:%S), %s (Unix timestamp, POSIX), %% a literal percent. %c, %x and %X are locale-dependent — avoid them for anything machine-readable.

asctime and ctime still exist and still return a static buffer with a trailing newline; both are deprecated in C23. Use strftime.

difftime and Comparing Times

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

int main(void)
{
    struct tm a = { .tm_year = 2026 - 1900, .tm_mon = 0, .tm_mday = 1, .tm_isdst = -1 };
    struct tm b = { .tm_year = 2026 - 1900, .tm_mon = 11, .tm_mday = 31, .tm_isdst = -1 };

    time_t start = mktime(&a);
    time_t end = mktime(&b);

    if (start == (time_t)-1 || end == (time_t)-1) {
        return 1;
    }

    // difftime is the ONLY portable way to subtract two time_t values -- time_t
    // need not be an integer count of seconds, and plain subtraction can overflow.
    double seconds = difftime(end, start);

    printf("%.0f seconds = %.1f days\n", seconds, seconds / 86400.0);
    return 0;
}

Measuring Elapsed Time

Which clock to use depends on the question:

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

static void workload(void)
{
    volatile double total = 0.0;
    for (int i = 1; i < 5000000; ++i) {
        total += 1.0 / i;
    }
}

int main(void)
{
    // WALL-CLOCK time: what the user experiences. Includes time spent blocked.
    struct timespec wall_start, wall_end;
    if (timespec_get(&wall_start, TIME_UTC) != TIME_UTC) {
        return 1;
    }

    // CPU time: what the process actually consumed. Excludes blocking, and on a
    // multi-threaded program may EXCEED the wall time.
    clock_t cpu_start = clock();

    workload();

    clock_t cpu_end = clock();
    if (timespec_get(&wall_end, TIME_UTC) != TIME_UTC) {
        return 1;
    }

    double wall = (double)(wall_end.tv_sec - wall_start.tv_sec)
                + (double)(wall_end.tv_nsec - wall_start.tv_nsec) / 1e9;
    double cpu = (double)(cpu_end - cpu_start) / CLOCKS_PER_SEC;

    printf("wall %.4f s, cpu %.4f s\n", wall, cpu);
    return 0;
}

Two caveats that invalidate naive measurements:

  • TIME_UTC is a wall clock, and wall clocks move. NTP adjustments, and manual changes, can make the end time earlier than the start. For durations, use a monotonic clock: C23’s optional TIME_MONOTONIC, or clock_gettime(CLOCK_MONOTONIC, …) on POSIX and QueryPerformanceCounter on Windows.

  • clock() measures processor time, so it under-reports anything I/O-bound and over-reports a program using several cores.

For benchmarking, prefer a purpose-built harness over hand-rolled timing: run the workload many times, discard warm-up, and report a distribution rather than one number. See Performance.

See Also