The C Path — learn C, visually

📚 The Standard Library

math.h & time.h: numbers and clocks

⏱ 14 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Square roots and angles power game physics, and a stopwatch tells you whether your code is actually fast or just feels fast — both live in today's two headers. Along the way you'll hit a rite of passage: your first sqrt program on Linux fails with an error message that has baffled C learners for decades, and you'll be one of the few beginners who knows exactly what it means — and that the fix is three characters long.

Two headers today: math.h, which gives C real mathematical muscle, and time.h, which answers both "what time is it?" and "how long did that take?". Plus the single most-Googled error message in C history — courtesy of the linker, the build stage from Part 0 that stitches compiled pieces into a program.

The math.h roster

familyfunctionsnotes
powerspow, sqrt, cbrt, hypothypot(a,b) = √(a²+b²) without overflow
trigsin, cos, tan, asin, acos, atan, atan2radians, not degrees! atan2(y,x) knows the quadrant
exp / logexp, log, log2, log10, exp2log is natural log (ln), not base 10
roundingfloor, ceil, round, truncfour different opinions about halves and negatives — see below
remainderfmod% for doubles: fmod(7.5, 2.0) → 1.5
miscfabs, fmin, fmaxfloat absolute value / min / max

Everything takes and returns double; f-suffixed variants (sqrtf, sinf…) work in float.

mathtour.c
#include <stdio.h>
#include <math.h>

int main(void) {
    printf("pow(2,10)   = %.0f\n", pow(2, 10));
    printf("sqrt(2)     = %.6f\n", sqrt(2));
    printf("cbrt(27)    = %.0f\n", cbrt(27));
    printf("hypot(3,4)  = %.0f\n", hypot(3, 4));
    printf("sin(pi/6)   = %.3f\n", sin(3.14159265358979 / 6));
    printf("log2(1024)  = %.0f\n", log2(1024));
    printf("fmod(7.5,2) = %.1f\n", fmod(7.5, 2));
    printf("floor(2.7)=%.0f ceil(2.1)=%.0f round(2.5)=%.0f\n",
           floor(2.7), ceil(2.1), round(2.5));
    return 0;
}
terminal
$ gcc mathtour.c -o mathtour -lm && ./mathtour
pow(2,10)   = 1024
sqrt(2)     = 1.414214
cbrt(27)    = 3
hypot(3,4)  = 5
sin(pi/6)   = 0.500
log2(1024)  = 10
fmod(7.5,2) = 1.5
floor(2.7)=2 ceil(2.1)=3 round(2.5)=3

🤔 What do floor(-2.5), ceil(-2.5), trunc(-2.5) and round(-2.5) each return?

Think first

floor(-2.5) = -3 (toward −∞) · ceil(-2.5) = -2 (toward +∞) · trunc(-2.5) = -2 (toward zero — this is what casting to int does) · round(-2.5) = -3 (halves round away from zero). Four functions, three different answers for one input — pick deliberately, especially for negative numbers.

The -lm rite of passage

On Linux, the math functions live in a separate library, libm. Including the header satisfies the compiler, but the linker still needs to be told where the code is — everyone hits this once:

the classic
$ gcc area.c -o area
/usr/bin/ld: /tmp/ccXty2.o: undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
# math.h declared sqrt, but the CODE lives in libm:
$ gcc area.c -o area -lm
$ ./area
r = 5.64
💡

Remember the stage model from Part 0: undefined reference is always the linker talking. The declaration (math.h) was fine; the definition lives in libm — append -lm to the command line. (Library flags go after your source files.)

🧠 Checkpoint: You included <math.h> but get undefined reference to `pow'. What’s wrong?

  • A typo in the include
  • The compiler is too old for pow
  • The linker wasn’t told to link libm — add -lm
  • pow needs C23
Show answer

The linker wasn’t told to link libm — add -lm — Headers only carry declarations. The implementation of the math functions is in a separate library on Linux, so the link step needs -lm after your source files. (Some calls with constant args get computed at compile time, which is why the error can appear "randomly".)

NaN, infinity & comparing floats

Floating-point math never crashes — it produces special values instead (Part 0 flashbacks): 1.0/0.0 gives INFINITY, sqrt(-1) gives NAN. Since NaN isn't equal to anything — including itself — you must test with isnan() and isinf(), never ==:

special.c
#include <stdio.h>
#include <math.h>

int main(void) {
    double inf = 1.0 / 0.0;        /* no crash — infinity   */
    double nan = sqrt(-1.0);       /* domain error — NaN    */

    printf("inf = %f, nan = %f\n", inf, nan);
    printf("nan == nan   -> %d\n", nan == nan);   /* false! */
    printf("isnan(nan)   -> %d\n", isnan(nan));
    printf("isinf(inf)   -> %d\n", isinf(inf));
    printf("isfinite(1.) -> %d\n", isfinite(1.0));

    double a = 0.1 + 0.2;
    printf("a == 0.3        -> %d\n", a == 0.3);
    printf("tolerance check -> %d\n", fabs(a - 0.3) < 1e-9);
    return 0;
}
terminal
$ gcc special.c -o special -lm && ./special
inf = inf, nan = -nan
nan == nan   -> 0
isnan(nan)   -> 1
isinf(inf)   -> 1
isfinite(1.) -> 1
a == 0.3        -> 0
tolerance check -> 1

And the golden rule stands: compare computed floats with a tolerance, e.g. fabs(a - b) < 1e-9, or relative to magnitude with DBL_EPSILON from float.h (next lesson digs into that header).

🧠 Checkpoint: Which expression reliably detects that x is NaN?

  • x == NAN
  • x != x
  • x == 0.0/0.0
  • x > INFINITY
Show answer

x != x — NaN is the only value not equal to itself, so x != x is true exactly for NaNs — that’s essentially how isnan() works. x == NAN is always false for the same reason. Prefer the readable isnan(x).

time.h: wall clocks

time(NULL) returns a time_t — on virtually every platform, seconds since the Unix epoch (Jan 1, 1970 UTC), though the standard only promises "some encoding"; portable code compares moments with difftime(t2, t1). To get human-readable parts, expand a time_t into a struct tm with localtime, then format it with strftime:

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

int main(void) {
    time_t now = time(NULL);            /* seconds since epoch */
    printf("raw time_t : %lld\n", (long long)now);

    struct tm *t = localtime(&now);     /* explode into fields */
    printf("year %d, month %d, day %d\n",
           t->tm_year + 1900,           /* years since 1900!   */
           t->tm_mon + 1,               /* 0-based months!     */
           t->tm_mday);

    char buf[64];
    strftime(buf, sizeof buf, "%A %Y-%m-%d %H:%M:%S", t);
    printf("formatted  : %s\n", buf);
    return 0;
}
terminal
$ gcc today.c -o today && ./today
raw time_t : 1754006400
year 2025, month 8, day 1
formatted  : Friday 2025-08-01 02:00:00
⚠️

struct tm quirks that ruin demos: tm_year is years since 1900, and tm_mon is 0-based (January = 0). Forget those and your program prints the year 126 or the wrong month. Also localtime returns a pointer to shared static storage — copy the struct if you need two at once.

time.h: stopwatches

For benchmarking, wall time is the wrong tool (other processes pollute it). clock() measures CPU time consumed by your process, in ticks of CLOCKS_PER_SEC:

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

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

    volatile double sum = 0;          /* volatile: don't optimize away */
    for (long i = 1; i <= 50000000L; i++)
        sum += 1.0 / i;

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

    printf("harmonic sum = %.6f\n", sum);
    printf("CPU time     = %.3f s\n", secs);
    return 0;
}
terminal
$ gcc -O2 bench.c -o bench && ./bench
harmonic sum = 18.304749
CPU time     = 0.184 s

C11 added timespec_get(&ts, TIME_UTC), which fills a struct timespec with seconds and nanoseconds — the portable way to get sub-second wall-clock timestamps.

🧠 Checkpoint: To measure how long your code took to compute, regardless of other programs hogging the machine, use…

  • time(NULL) before and after
  • clock() before and after, divided by CLOCKS_PER_SEC
  • strftime
  • difftime on two time_t values
Show answer

clock() before and after, divided by CLOCKS_PER_SEC — clock() counts CPU time your process actually consumed; wall-clock time (time/difftime) includes everything else running. For sub-second wall timestamps, C11’s timespec_get gives nanosecond resolution.

This spot has an interactive editor widget — open the interactive lesson to play with it.

Next up: three small headers with outsized importance — character tests, assertions, and the errno error-reporting convention.

▶ Practice this lesson interactively (with live gcc)