C time.h Library

The <time.h> header is the C standard library’s toolkit for working with dates, calendar time, and elapsed processing time. It gives you a small set of types and functions to read the current date and time, break a timestamp down into year/month/day/hour fields, format it for display, and measure how long parts of your program take to run. Almost every non-trivial C program that logs events, schedules work, or benchmarks performance eventually reaches for time.h.

Overview / How it works

Under the hood, most systems track time as a single number: the count of seconds that have elapsed since the Unix Epoch, midnight, January 1, 1970 (UTC). C exposes this as the opaque type time_t. It’s typically a signed integer (commonly a 64-bit integer on modern systems, though older systems used 32 bits, which caused the famous “Year 2038 problem” for 32-bit signed types). Because time_t is just a number, it’s easy to store, compare, and subtract, but it is not human-readable by itself.

To make a timestamp human-readable, C provides struct tm, which breaks a time_t into individual calendar fields: year, month, day, hour, minute, second, day of week, and day of year. Converting between the two representations happens through functions like localtime() (which also adjusts for your system’s local timezone and daylight saving rules) and gmtime() (which gives you the same breakdown in UTC, ignoring timezone). The reverse conversion, from a filled-in struct tm back to a time_t, is done by mktime().

Separately, time.h also offers clock(), which measures approximate processor time used by your program (not wall-clock time), reported in units of CLOCKS_PER_SEC per second. This is useful for rough performance measurements of CPU-bound code, though it does not account for time spent waiting on I/O or other processes.

A key internal detail: functions like localtime(), gmtime(), asctime(), and ctime() typically return a pointer into a single, statically allocated struct tm or character buffer owned by the library. Every call overwrites the same memory. This means the pointer returned by one call becomes invalid (or silently changes) the moment you call any of these functions again, and none of them are safe to use from multiple threads simultaneously without extra synchronization.

Syntax

#include <time.h>

time_t t = time(NULL);              /* current calendar time */
struct tm *local = localtime(&t);  /* break down to local time fields */
struct tm *utc   = gmtime(&t);     /* break down to UTC fields */
time_t back = mktime(local);        /* struct tm -> time_t */
double secs = difftime(t2, t1);     /* seconds between two time_t values */
clock_t c = clock();                 /* processor time used so far */
char buf[64];
strftime(buf, sizeof(buf), "%Y-%m-%d", local); /* format struct tm as text */
  • time_t time(time_t *arg) — returns the current calendar time as seconds since the Epoch. Pass NULL if you don’t need the out-parameter, or pass the address of a time_t to also store the result there.
  • struct tm *localtime(const time_t *t) — converts a time_t to a broken-down local time. Returns a pointer to a statically allocated struct (reused on every call).
  • struct tm *gmtime(const time_t *t) — same as localtime() but in UTC, with no timezone adjustment.
  • time_t mktime(struct tm *tm) — converts a broken-down local time back into a time_t. Also normalizes out-of-range fields (e.g. tm_mday = 32 rolls into the next month).
  • double difftime(time_t t2, time_t t1) — returns t2 - t1 in seconds as a double. Prefer this over raw subtraction for portability.
  • clock_t clock(void) — returns processor time used by the program; divide by CLOCKS_PER_SEC to get seconds.
  • size_t strftime(char *s, size_t max, const char *fmt, const struct tm *tm) — formats a struct tm into a text buffer using format specifiers similar to printf.
  • char *ctime(const time_t *t) / char *asctime(const struct tm *tm) — quick, fixed-format (25-character, English, US-style) time strings ending in a newline.

struct tm fields

Field Meaning Range
tm_sec Seconds after the minute 0-60 (61 allows a leap second)
tm_min Minutes after the hour 0-59
tm_hour Hours since midnight 0-23
tm_mday Day of the month 1-31
tm_mon Months since January 0-11 (0 = January!)
tm_year Years since 1900 e.g. 126 for 2026
tm_wday Days since Sunday 0-6
tm_yday Days since Jan 1 0-365
tm_isdst Daylight saving flag >0, 0, or <0

Examples

Example 1: Getting and printing the current time

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

int main(void) {
    time_t now;
    time(&now);

    printf("Seconds since Epoch: %ld\n", (long)now);
    printf("Current time: %s", ctime(&now));

    return 0;
}

Output (values will differ every time you run it, since it reflects the real current time):

Seconds since Epoch: 1784419200
Current time: Sat Jul 18 12:00:00 2026

time(&now) stores the raw Epoch count in now. ctime() takes the address of that time_t and produces a ready-to-print, fixed-format string — note that ctime()‘s string already ends with a newline, so the printf format string doesn’t add another one.

Example 2: Breaking down and formatting time with struct tm and strftime()

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

int main(void) {
    time_t now = time(NULL);
    struct tm *local = localtime(&now);

    char buffer[64];
    strftime(buffer, sizeof(buffer), "%A, %B %d, %Y - %H:%M:%S", local);

    printf("Formatted date/time: %s\n", buffer);
    printf("Year: %d, Month: %d, Day: %d\n",
           local->tm_year + 1900, local->tm_mon + 1, local->tm_mday);

    return 0;
}

Output (exact date/time text will vary depending on when you run it):

Formatted date/time: Saturday, July 18, 2026 - 14:32:07
Year: 2026, Month: 7, Day: 18

localtime() fills a struct tm with local calendar fields. strftime() then renders it as text using format codes (%A full weekday name, %B full month name, %Y 4-digit year, and so on). Note the + 1900 and + 1 corrections: tm_year counts from 1900, and tm_mon counts months from 0.

Example 3: Measuring elapsed CPU time with clock()

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

int main(void) {
    clock_t start = clock();

    long sum = 0;
    for (long i = 0; i < 200000000L; i++) {
        sum += i % 7;
    }

    clock_t end = clock();
    double elapsed = (double)(end - start) / CLOCKS_PER_SEC;

    printf("Sum: %ld\n", sum);
    printf("Time taken: %f seconds\n", elapsed);

    return 0;
}

Output (the sum is fixed, but the elapsed time depends on your CPU speed and compiler optimizations):

Sum: 599999994
Time taken: 0.550000 seconds

clock() returns clock ticks used by the process. Calling it once before and once after the work, subtracting, and dividing by CLOCKS_PER_SEC converts the tick count into seconds. This measures CPU time, not real-world wall-clock time — if the process is paused or waiting on disk/network I/O, that time is not counted.

Under the hood: how a timestamp becomes text

  1. The operating system tracks time internally (often as seconds and nanoseconds since the Epoch, using a hardware clock or NTP-synced software clock).
  2. time() asks the OS for the current value and returns it as a time_t, an opaque arithmetic type — internally just an integer count of seconds.
  3. localtime() takes that integer and, using the system’s timezone database, works out what “wall clock” date and time it corresponds to where the program is running, filling in each field of a struct tm (year, month, day, and so on) and applying daylight saving adjustments if applicable.
  4. strftime() walks your format string character by character. When it hits an ordinary character, it copies it. When it hits a % specifier, it looks up the matching field in the struct tm and writes the formatted text (padding numbers, spelling out month/weekday names from the current locale, etc.) into your buffer.
  5. If the destination buffer isn’t big enough, strftime() returns 0 and leaves the buffer’s contents undefined — always size buffers generously and check the return value in production code.

Common Mistakes

Mistake 1: Forgetting tm_mon is zero-based.

/* Wrong: sets month to July when tm_mon = 6 means July, not tm_mon = 7 */
struct tm t = {0};
t.tm_mon = 7;   /* this is actually August! */
t.tm_mday = 18;
t.tm_year = 2026 - 1900;

Because tm_mon counts from 0 (January = 0), setting it to 7 actually means August. To represent July, use 6:

struct tm t = {0};
t.tm_mon = 6;   /* July */
t.tm_mday = 18;
t.tm_year = 2026 - 1900;

Mistake 2: Reusing the pointer from localtime()/ctime() after a second call.

/* Wrong: both pointers alias the same static buffer */
time_t t1 = time(NULL);
struct tm *a = localtime(&t1);
time_t t2 = t1 + 3600;
struct tm *b = localtime(&t2);
/* by now, *a has ALSO changed, because a and b point to the same memory! */
printf("%d vs %d\n", a->tm_hour, b->tm_hour);  /* prints the same value twice */

Fix it by copying the struct out immediately after each call, before making the next call:

time_t t1 = time(NULL);
struct tm a = *localtime(&t1);   /* copy the struct, not just the pointer */
time_t t2 = t1 + 3600;
struct tm b = *localtime(&t2);
printf("%d vs %d\n", a.tm_hour, b.tm_hour);

Mistake 3: Using clock() to measure wall-clock/real-world elapsed time. clock() reports CPU time consumed, which can be much less than real elapsed time if the program sleeps or waits on I/O. For measuring real elapsed time (e.g. how long a network request took), use a wall-clock API such as time() for second-level precision, or platform-specific high-resolution timers for finer precision.

Best Practices

  • Always use difftime() instead of subtracting two time_t values directly — it’s portable even if time_t‘s underlying representation isn’t a simple integer.
  • Copy the struct tm returned by localtime()/gmtime() into your own variable immediately if you need to keep it around or call these functions again before you’re done with it.
  • Check strftime()‘s return value; a return of 0 means the buffer was too small and its contents are unreliable.
  • Prefer strftime() over asctime()/ctime() for anything beyond quick debugging — the fixed format of the latter two can’t be localized or customized, and they can overflow on some non-standard years.
  • Remember tm_year is years since 1900 and tm_mon is zero-based; add/subtract accordingly every time you touch these fields.
  • Use clock() only for coarse, CPU-bound benchmarking within a single run; don’t rely on it for cross-run wall-clock comparisons or multi-threaded timing.

Practice Exercises

  1. Write a program that prints the current date in the format YYYY-MM-DD using strftime().
  2. Write a program that builds a struct tm for a specific date you choose (e.g. December 25, 2026), converts it to a time_t with mktime(), and then uses difftime() to print how many days remain until that date from “now”.
  3. Write a program that times how long it takes to sort an array of 100,000 random integers (using any sorting approach) with clock(), printing the elapsed time in seconds.

Summary

  • time_t stores calendar time as seconds since the Unix Epoch; struct tm breaks it into readable fields.
  • time() gets the current time; localtime()/gmtime() convert it to broken-down local/UTC time; mktime() converts back.
  • strftime() is the flexible, locale-aware way to format dates and times as text; prefer it over ctime()/asctime() for anything beyond quick debugging.
  • difftime() safely computes the number of seconds between two time_t values.
  • clock() measures approximate CPU time used by your process, useful for rough performance benchmarking, but it is not wall-clock time.
  • Functions like localtime() and ctime() return pointers to shared static storage — copy the data out before calling them again.
  • tm_year is years since 1900 and tm_mon is zero-based; both are common sources of off-by-one bugs.