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_MAXbe 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:
getrandomon Linux,arc4random_bufon the BSDs and macOS,BCryptGenRandomon 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
-
Basic Types and Values — integer ranges, promotions and floating-point types.
-
Operators and Expressions — the bitwise operators
<stdbit.h>complements. -
Type-Generic Programming —
<tgmath.h>and_Generic. -
Error Handling and Program Failure — arithmetic wrongdoings in full.
-
C++: Numbers and Math — the same math core, plus
<numbers>,<bit>and the<random>engines.
References
-
WG14 N3220 — the C23 working draft (§7.12
<math.h>, §7.6<fenv.h>, §7.3<complex.h>, §7.18<stdbit.h>, §7.20<stdckdint.h>, §7.24.2 "Pseudo-random sequence generation"). -
GNU C Library Reference Manual — Generating Unpredictable Bytes (
getrandom).