C Mastery / Types Part 2: Floating-Point and IEEE 754
Part 1 — The Core Language

Types Part 2: Floating-Point and IEEE 754

This chapter covers float, double, and long double, the IEEE 754 representation that underlies them on virtually every real system, and the special values (NaN, infinities, signed zero) that make floating-point arithmetic fundamentally different from integer arithmetic.

Why This Matters

Floating-point arithmetic is *not* real-number arithmetic. It is a finite, approximate system with rounding, special values, and specific failure modes. Treating it as exact leads to equality bugs, accumulation of error, and security issues. Understanding the representation lets you predict and control these behaviors.

Prerequisites

Core Concept

The three floating types

TypeTypical precisionTypical bits
float24 significant bits (~7 decimal digits)32
double53 significant bits (~15–16 decimal digits)64
long doubleat least double; often 80-bit or 128-bitimplementation-defined

The standard only guarantees that long double has at least the range and precision of double. float is the least precise, double is the default for literals and most math, and long double is for extended precision.

Floating literals

An unsuffixed floating literal has type double. Suffixes select types:

1.0     /* double */
1.0f    /* float */
1.0L    /* long double */
1e3     /* double (1000.0) */
1.5e-2  /* double (0.015) */

IEEE 754

IEEE 754 is the standard for floating-point representation and arithmetic. C does not require IEEE 754, but virtually every modern compiler and CPU implements it for float and double. VERIFIED (as a de facto statement; the C standard makes it optional).

How It Works

Binary floating-point layout

A binary floating-point number is represented as three fields:

[sign bit][exponent bits][fraction (significand) bits]

The value (for normal numbers) is:

(-1)^sign × (1.fraction) × 2^(exponent - bias)

For double (64-bit binary64):

For float (32-bit binary32):

The leading 1. is implicit for *normal* numbers. *Subnormal* numbers use 0.fraction to represent values very close to zero, sacrificing precision to avoid underflow to zero.

Special values

ValueHow representedMeaning
+0.0 / -0.0exponent and fraction all zero, sign differssigned zero
+Inf / -Infexponent all ones, fraction zeroinfinity
NaNexponent all ones, fraction nonzeronot-a-number

These values arise from real operations: 1.0/0.0 is +Inf, 0.0/0.0 is NaN, and sqrt(-1.0) is NaN (with <math.h>).

Signed zero

IEEE 754 has both +0.0 and -0.0. They compare equal (+0.0 == -0.0 is true), but 1.0/+0.0 is +Inf while 1.0/-0.0 is -Inf. This distinction matters in branch cuts and some numerical algorithms.

Rounding

Real results that are not exactly representable are rounded to the nearest representable value, by default "round to nearest, ties to even." The rounding mode can be queried and changed with <fenv.h>, but changing it is advanced and rarely needed.

Syntax

Declaring floats

float  f = 1.0f;
double d = 1.0;
long double ld = 1.0L;

Testing special values (C99)

#include <math.h>
#include <float.h>

int main(void)
{
    double x = 0.0 / 0.0;   /* NaN */
    if (isnan(x)) { /* ... */ }
    if (isinf(x)) { /* ... */ }
    if (isfinite(x)) { /* ... */ }
    return 0;
}

Do not test x == x to detect NaN portably; use isnan. (The x != x trick works for NaN in IEEE 754, but isnan is the standard, readable form.)

Examples

Comparing for near-equality

Never compare floating-point numbers with == for values computed through different paths. Use an epsilon:

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

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

    printf("a == b : %d\n", a == b);
    printf("close  : %d\n", fabs(a - b) < 1e-9);
    return 0;
}

Expected output (typical IEEE 754):

a == b : 0
close  : 1

0.1 + 0.2 is not exactly 0.3 because 0.1, 0.2, and 0.3 are not exactly representable in binary.

Accumulation of error

#include <stdio.h>

int main(void)
{
    float  f = 0.1f;
    double d = 0.1;

    printf("float  sum: %.10f\n", f + f + f);
    printf("double sum: %.20f\n", d + d + d);
    return 0;
}

The float version loses precision sooner than the double version. Exact output is implementation-defined (formatting), but the point is the visible rounding.

Variations

Extended precision (long double)

On x86, long double is often the 80-bit extended format; on ARM64 and many other platforms it is 128-bit (binary128) or the same as double. LDBL_MANT_DIG in <float.h> tells you the precision.

Decimal floating-point (rare)

C23 adds optional decimal floating types (_Decimal32, _Decimal64, _Decimal128). These are niche and not covered in depth here; know that they exist and are distinct from binary floating-point. C23

Common Mistakes

Undefined Behavior

raise floating-point exceptions, which are a separate mechanism from C "undefined behavior").

value is out of range of the integer type (e.g., (int)1e30). VERIFIED

violates strict aliasing and is UB.

Portability

modern general-purpose systems. VERIFIED

representable value (machine epsilon).

Under the Hood

On x86-64, float and double operations use SSE/AVX scalar instructions (addss, addsd, etc.). ARM64 has its own scalar FP instructions. The IEEE 754 layout is implemented directly in hardware, which is why operations are fast and deterministic for a given rounding mode.

The compiler is allowed to contract expressions (e.g., a*b+c into a fused multiply-add) unless you disable it with -ffp-contract=off (GCC/Clang) or the equivalent. FMA can produce different rounding than separate multiply and add. COMPILER-SPECIFIC

Practical Usage

reduced precision is acceptable.

about the algorithm so exact comparison is unnecessary.

Exercises

1. Write a program that prints FLT_RADIX, FLT_MANT_DIG, DBL_MANT_DIG, LDBL_MANT_DIG, and the machine epsilons. Identify your platform's floating format. 2. Demonstrate that 0.1 + 0.2 != 0.3 and explain why at the bit level. 3. Write a function that returns the absolute difference between two doubles and explain when comparing it to a fixed epsilon is wrong. 4. Show the difference between float and double accumulation over a large loop.

Deep Challenge

Implement (in C) a function that prints the IEEE 754 bit pattern of a double as sign/exponent/fraction, using only well-defined bit operations on the object representation (memcpy into a uint64_t, or a union — see c.union.punning for the rules). Then decode a few special values and verify your output matches isnan/isinf/isfinite.

References

Verification

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.types.float05