Numbers and Math

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.

C’s numeric library spans four headers of integer support and four of floating-point support. The two C23 additions — <stdckdint.h> for overflow-checked arithmetic and <stdbit.h> for bit manipulation — standardize what every serious codebase previously hand-rolled.

Integer Functions — <stdlib.h>

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("%d %ld %lld\n", abs(-5), labs(-5L), llabs(-5LL));
    printf("%" PRIdMAX "\n", imaxabs((intmax_t)-5));

    // Quotient and remainder in one operation, with a defined sign convention.
    div_t d = div(-7, 2);
    ldiv_t ld = ldiv(-7L, 2L);

    printf("%d %d | %ld %ld\n", d.quot, d.rem, ld.quot, ld.rem);   // -3 -1 | -3 -1
    return 0;
}

abs(INT_MIN) is undefined — the negation overflows — which is the one trap in this family.

Format Macros — <inttypes.h>

Fixed-width types have no fixed printf conversion, because int64_t may be long or long long. The macros expand to the right one:

#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    int64_t big = 9007199254740993;
    uint32_t small = 4000000000u;
    uintptr_t address;
    int32_t parsed = 0;

    address = (uintptr_t)&big;

    printf("%" PRId64 " %" PRIu32 " %" PRIxPTR "\n", big, small, address);

    // The SCN* macros do the same for scanf.
    if (sscanf("-12345", "%" SCNd32, &parsed) == 1) {
        printf("parsed %" PRId32 "\n", parsed);
    }

    // size_t and ptrdiff_t have their own built-in specifiers -- no macro needed.
    size_t count = 3;
    ptrdiff_t delta = -2;
    printf("%zu %td\n", count, delta);
    return 0;
}

The naming is systematic: PRI (print) or SCN (scan), then the conversion (d, i, u, o, x, X), then the type (8, 16, 32, 64, MAX, PTR, or LEAST/FAST variants).

Checked Arithmetic — <stdckdint.h> (C23)

Signed overflow is undefined behavior, so detecting it portably used to require pre-checking against INT_MAX. C23 provides the operations directly:

#include <limits.h>
#include <stdckdint.h>
#include <stdio.h>

int main(void)
{
    int result = 0;

    // Each returns true if the mathematical result does NOT fit the destination.
    if (ckd_add(&result, INT_MAX, 1)) {
        printf("addition overflowed; wrapped value is %d\n", result);
    }

    if (!ckd_mul(&result, 1000, 1000)) {
        printf("1000 * 1000 = %d\n", result);
    }

    if (ckd_sub(&result, INT_MIN, 1)) {
        puts("subtraction overflowed");
    }

    // The destination type is what matters -- narrowing is checked too:
    signed char small = 0;
    if (ckd_add(&small, (signed char)100, (signed char)100)) {
        printf("does not fit a signed char: %d\n", small);
    }
    return 0;
}

The allocation-size guard becomes clean and correct:

#include <stdckdint.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

static void *allocate_array(size_t count, size_t element_size)
{
    size_t total = 0;

    if (ckd_mul(&total, count, element_size)) {
        return nullptr;                     // the multiplication would have wrapped
    }
    return malloc(total);
}

int main(void)
{
    void *ok = allocate_array(100, sizeof(double));
    void *nope = allocate_array(SIZE_MAX, 2);

    printf("%s %s\n", ok != nullptr ? "allocated" : "failed",
                      nope == nullptr ? "rejected" : "allocated");
    free(ok);
    return 0;
}

Before C23, GCC and Clang offered __builtin_add_overflow and friends with the same semantics — and still do, so #if __has_include(<stdckdint.h>) with a builtin fallback covers both.

Bit Utilities — <stdbit.h> (C23)

#include <stdbit.h>
#include <stdio.h>

int main(void)
{
    unsigned int value = 0b0010'1100u;      // 44

    printf("count_ones      = %u\n", stdc_count_ones(value));           // 3
    printf("count_zeros     = %u\n", stdc_count_zeros(value));
    printf("leading_zeros   = %u\n", stdc_leading_zeros(value));
    printf("trailing_zeros  = %u\n", stdc_trailing_zeros(value));       // 2
    printf("first_leading_one  = %u\n", stdc_first_leading_one(value));
    printf("first_trailing_one = %u\n", stdc_first_trailing_one(value));
    printf("bit_width       = %u\n", stdc_bit_width(value));            // 6
    printf("has_single_bit  = %d\n", (int)stdc_has_single_bit(value));  // 0
    printf("bit_floor       = %u\n", stdc_bit_floor(value));            // 32
    printf("bit_ceil        = %u\n", stdc_bit_ceil(value));             // 64

    // Endianness, finally testable at compile time:
#if __STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_LITTLE__
    puts("little-endian");
#elif __STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_BIG__
    puts("big-endian");
#else
    puts("mixed-endian");
#endif
    return 0;
}

These are type-generic over the unsigned integer types and compile to a single instruction (popcnt, lzcnt, bsr) where the hardware has one. stdc_bit_ceil is the "round up to a power of two" that hash tables and allocators need, and it is undefined if the result would not fit — check stdc_bit_width first.

Real Maths — <math.h>

Classification and Comparison

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

static const char *classify(double x)
{
    switch (fpclassify(x)) {
    case FP_NAN:       return "nan";
    case FP_INFINITE:  return "infinite";
    case FP_ZERO:      return "zero";
    case FP_SUBNORMAL: return "subnormal";
    case FP_NORMAL:    return "normal";
    default:           return "unknown";
    }
}

int main(void)
{
    printf("%s %s %s %s\n", classify(NAN), classify(INFINITY), classify(0.0), classify(1.5));

    printf("isnan=%d isinf=%d isfinite=%d isnormal=%d signbit=%d\n",
           isnan(NAN), isinf(-INFINITY), isfinite(1.0), isnormal(1.0), signbit(-0.0) != 0);

    // NaN is unordered: every comparison with it is false, including NAN == NAN.
    printf("NAN == NAN is %d; use isnan instead\n", NAN == NAN);

    // Never compare floating-point values for exact equality:
    double a = 0.1 + 0.2;
    printf("a == 0.3 ? %d; |a - 0.3| < 1e-9 ? %d\n", a == 0.3, fabs(a - 0.3) < 1e-9);
    return 0;
}

Rounding

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

int main(void)
{
    double values[] = { 2.5, -2.5, 2.4, 2.6 };

    for (size_t i = 0; i < sizeof values / sizeof values[0]; ++i) {
        double v = values[i];
        printf("%5.1f  floor %5.1f  ceil %5.1f  trunc %5.1f  round %5.1f  nearbyint %5.1f\n",
               v, floor(v), ceil(v), trunc(v), round(v), nearbyint(v));
    }

    printf("lround(2.5) = %ld, llround(-2.5) = %lld\n", lround(2.5), llround(-2.5));

    double integral = 0.0;
    double fraction = modf(3.75, &integral);
    printf("modf(3.75) -> %g + %g\n", integral, fraction);
    return 0;
}

round rounds half away from zero; nearbyint and rint use the current rounding mode (round-to-nearest, ties-to-even by default), which is why nearbyint(2.5) is 2 while round(2.5) is 3.

fma and Precision

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

int main(void)
{
    double a = 0.1, b = 0.2, c = 0.3;

    // fma computes a * b + c with ONE rounding instead of two -- more accurate,
    // and a single instruction on any modern CPU.
    printf("a*b + c = %.20g\n", a * b + c);
    printf("fma     = %.20g\n", fma(a, b, c));

    // hypot avoids the overflow that sqrt(x*x + y*y) suffers on large inputs.
    printf("hypot(3,4) = %g, hypot(1e200,1e200) = %g\n", hypot(3.0, 4.0), hypot(1e200, 1e200));

    // log1p/expm1 keep precision near zero where log(1+x)/exp(x)-1 lose it.
    printf("log1p(1e-16) = %g vs log(1+1e-16) = %g\n", log1p(1e-16), log(1.0 + 1e-16));
    return 0;
}

Error Handling

#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    // math_errhandling says which mechanism the implementation uses.
    printf("errno reporting: %d, fp exceptions: %d\n",
           (math_errhandling & MATH_ERRNO) != 0,
           (math_errhandling & MATH_ERREXCEPT) != 0);

    errno = 0;                              // clear BEFORE the call
    double domain = sqrt(-1.0);             // domain error
    if (errno == EDOM) {
        printf("sqrt(-1) -> %g (%s)\n", domain, strerror(errno));
    }

    errno = 0;
    double range = exp(10000.0);            // range error: overflow
    if (errno == ERANGE) {
        printf("exp(10000) -> %g (%s)\n", range, strerror(errno));
    }
    return 0;
}

Remember to link the maths library on Unix: -lm. GCC and Clang do not add it automatically, and the resulting error appears at link time (undefined reference to 'sqrt').

The Floating-Point Environment — <fenv.h>

#include <fenv.h>
#include <math.h>
#include <stdio.h>

// Required whenever a program inspects or changes the FP environment:
#pragma STDC FENV_ACCESS ON

int main(void)
{
    feclearexcept(FE_ALL_EXCEPT);

    volatile double tiny = 1.0 / 3.0;       // inexact
    volatile double overflowed = 1e308 * 10.0;
    (void)tiny;
    (void)overflowed;

    if (fetestexcept(FE_INEXACT)) {
        puts("FE_INEXACT raised");
    }
    if (fetestexcept(FE_OVERFLOW)) {
        puts("FE_OVERFLOW raised");
    }

    // Change the rounding mode, then restore it.
    int previous = fegetround();
    if (fesetround(FE_TOWARDZERO) == 0) {
        printf("toward zero: nearbyint(2.7) = %g\n", nearbyint(2.7));
        fesetround(previous);
    }
    printf("restored:    nearbyint(2.7) = %g\n", nearbyint(2.7));
    return 0;
}

Compile such code with -frounding-math (GCC/Clang) so the optimizer does not fold constants using the default mode, and never with -ffast-math, which discards the guarantees this header exists to provide.

Complex Numbers — <complex.h>

#include <complex.h>
#include <stdio.h>

int main(void)
{
    double complex z = 3.0 + 4.0 * I;
    double complex w = 1.0 - 2.0 * I;

    double complex sum = z + w;
    double complex product = z * w;

    printf("z = %g%+gi, |z| = %g, arg = %g\n", creal(z), cimag(z), cabs(z), carg(z));
    printf("sum = %g%+gi, product = %g%+gi\n",
           creal(sum), cimag(sum), creal(product), cimag(product));
    printf("conj = %g%+gi, sqrt = %g%+gi\n",
           creal(conj(z)), cimag(conj(z)), creal(csqrt(z)), cimag(csqrt(z)));
    return 0;
}

<complex.h> is optional (__STDC_NO_COMPLEX__); MSVC notably does not provide the _Complex type, offering its own struct-based _Dcomplex instead. C23 adds CMPLX macros for constructing values with an exact signed zero or NaN part.

Pseudo-Random Numbers

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

// Unbiased range: rejection sampling, NOT rand() % n (which favours low values
// whenever n does not divide RAND_MAX + 1).
// A single rand() call spans 0..RAND_MAX, so a larger bound cannot be served:
// reject it up front instead of looping forever on an empty acceptance range.
static bool random_below(int bound, int *out)
{
    if (bound <= 0 || bound > RAND_MAX) {
        return false;
    }

    int buckets = RAND_MAX / bound;
    int limit = buckets * bound;

    int value;
    do {
        value = rand();
    } while (value >= limit);

    *out = value / buckets;
    return true;
}

int main(void)
{
    srand((unsigned)time(nullptr));         // seed ONCE, at start-up

    printf("RAND_MAX = %d\n", RAND_MAX);
    for (int i = 0; i < 5; ++i) {
        int roll = 0;
        if (random_below(6, &roll)) {
            printf("%d ", roll + 1);        // a fair die
        }
    }
    putchar('\n');
    return 0;
}

Three things about rand:

  • The quality is unspecified. The standard requires only that RAND_MAX be at least 32767; it says nothing about the period or the statistical quality, and historically some implementations were dreadful in the low bits.

  • It is not thread-safe (it has hidden state) and not reproducible across implementations even from the same seed.

  • It is not cryptographically secure. For keys, tokens, nonces or anything an attacker should not predict, use the OS: getrandom on Linux, arc4random_buf on the BSDs and macOS, BCryptGenRandom on Windows.

For simulation work needing reproducibility across platforms, implement a named generator (PCG, xoshiro) or use a library — do not rely on rand.

See Also