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
c.core.3— integer types and representations.c.core.2— objects and the abstract machine.
Core Concept
The three floating types
| Type | Typical precision | Typical bits |
|---|---|---|
float | 24 significant bits (~7 decimal digits) | 32 |
double | 53 significant bits (~15–16 decimal digits) | 64 |
long double | at least double; often 80-bit or 128-bit | implementation-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):
- 1 sign bit
- 11 exponent bits
- 52 fraction bits
- bias = 1023
For float (32-bit binary32):
- 1 sign bit
- 8 exponent bits
- 23 fraction bits
- bias = 127
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
| Value | How represented | Meaning |
|---|---|---|
+0.0 / -0.0 | exponent and fraction all zero, sign differs | signed zero |
+Inf / -Inf | exponent all ones, fraction zero | infinity |
NaN | exponent all ones, fraction nonzero | not-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
- Comparing floats with
==. - Assuming
0.1is exact. - Using
floatwhen accumulated error matters; preferdouble. - Assuming
long doublehas a specific size across platforms. - Ignoring NaN propagation: any arithmetic with NaN yields NaN.
Undefined Behavior
- Floating-point operations themselves are generally well-defined (they may
raise floating-point exceptions, which are a separate mechanism from C "undefined behavior").
- Converting a floating value to an integer type is undefined behavior if the
value is out of range of the integer type (e.g., (int)1e30). VERIFIED
- Casting between floating types and unrelated object types via pointer casts
violates strict aliasing and is UB.
Portability
- IEEE 754 is not required by the C standard but is universal in practice on
modern general-purpose systems. VERIFIED
FLT_RADIXin<float.h>tells you the radix (2 on IEEE systems).FLT_MANT_DIG,DBL_MANT_DIG,LDBL_MANT_DIGgive precision.FLT_EPSILON,DBL_EPSILONgive the difference between 1.0 and the next
representable value (machine epsilon).
FLT_MAX,DBL_MAX,FLT_MIN,DBL_MINgive range limits.long doublevaries most across platforms.
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
- Use
doubleas the default floating type. - Use
floatonly when memory bandwidth or storage size dominates and the
reduced precision is acceptable.
- For money, use integer cents or a decimal type, not binary floating-point.
- For equality, use an epsilon relative to the magnitudes involved, or reason
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.
Related Concepts
c.types.int— integer types (for comparisons and conversions).c.stdlib.6— math.h functions.c.union.punning— reading object representation via unions.c.types.uac— conversions involving floats.
References
- ISO/IEC 9899:2018 §6.2.5, §5.2.4.2.2, §7.12 (math.h), §7.6 (fenv.h).
- IEEE 754-2019.
<float.h>documentation for your platform.
Verification
- IEEE 754 is not required by C but is the de facto standard.
VERIFIED - The binary32/binary64 layouts described are IEEE 754.
VERIFIED - Float-to-integer conversion out of range is UB.
VERIFIED 0.1 + 0.2 != 0.3is a consequence of binary representation.VERIFIED- No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] float, double, long double
- [ ] Floating literals and suffixes
- [ ] IEEE 754 layout
- [ ] Normal vs. subnormal numbers
- [ ] NaN, infinity, signed zero
- [ ] Rounding and machine epsilon
- [ ] float.h macros
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.types.float | 0 | 5 |