C / TYPES AND REPRESENTATION
float, double and why decimals cannot be exact
Predict why decimal literals like 0.1 cannot be stored exactly in float or double, and compare floating-point results with a tolerance instead of ==.
What you will learn
- Explain why 1/10 is a repeating binary fraction and so has no exact double
- Use printf("%.17f") to see the value a double really holds
- Replace == on computed doubles with a fabs() difference against a tolerance
- Choose double by default; float keeps only about 7 significant digits
Understanding float, double and why decimals cannot be exact
A double does not store decimal digits. It stores a sign, a 53-bit significand (52 bits in memory plus an implied leading 1) and a power-of-two exponent, so every value it can hold has the form m * 2^e. A decimal fraction fits that form only when its denominator is a power of two: 0.5 is 1*2^-1, 0.375 is 3*2^-3. One tenth is 1/(2*5), and the factor 5 never divides a power of two, so in binary it is the repeating pattern 0.0001100110011... which must be cut off after 53 bits; the compiler stores the nearest neighbour instead, 0.1000000000000000055511151231257827.
Arithmetic adds a second source of error, because the exact result of adding two doubles is usually not a double either and gets rounded again. The stored 0.1 and 0.2 sum to a value sitting exactly halfway between two doubles, and the tie-break rule (round to the even significand) pushes it one step above the double the compiler chose for the literal 0.3, so the two sides of 0.1 + 0.2 == 0.3 differ by 2^-54, about 5.6e-17. The default %f conversion rounds to six decimals and prints 0.300000 for both, which is why this normally surfaces as a comparison that never becomes true rather than as strange output.
float and double differ only in how the bits are split: float has 24 significand bits (about 7 decimal digits, FLT_DIG is 6) and double has 53 (about 15 to 17 digits, DBL_DIG is 15). More bits shrink the gap between neighbouring values but never make 1/10 representable, so moving from float to double buys headroom, not exactness. In C an unsuffixed literal such as 0.1 is a double and 0.1f is a float, and a float passed to printf is promoted back to double, so %f is the right specifier for both.
<stdio.h>
int main(void)
{
double sum = 0.1 + 0.2;
printf("0.1 + 0.2 = %.17f\n", sum);
printf("0.3 = %.17f\n", 0.3);
printf("sum == 0.3 : %d\n", sum == 0.3);
printf("difference : %g\n", sum - 0.3);
float f = 0.1f;
printf("0.1f = %.10f\n", f);
printf("0.1 = %.10f\n", 0.1);
return 0;
}
A double holds a binary fraction times a power of two, so any decimal that is not a sum of powers of two is silently replaced by its nearest representable neighbour, and every operation rounds again.
Worked examples
Adding 0.1 ten times
Repeated addition rounds at every step, so ten additions of 0.1 land just below 1.0.
<math.h>
<stdio.h>
int main(void)
{
double total = 0.0;
int i;
for (i = 0; i < 10; i++)
total += 0.1;
printf("total = %.17f\n", total);
printf("total == 1.0 : %d\n", total == 1.0);
printf("within 1e-9 : %d\n", fabs(total - 1.0) <= 1e-9);
return 0;
}
Example explained
Line 1total += 0.1 rounds once per iteration, because the exact sum of the running total and the stored 0.1 is normally not itself a double.
Line 2The final value is 1 - 2^-53, the largest double below 1.0, which is why %.17f shows a run of nines.
Line 3total == 1.0 yields 0: the result is one representable step short of 1.0, not close enough to be the same value.
Line 4fabs(total - 1.0) <= 1e-9 yields 1 because the drift is only about 1.1e-16; fabs needs <math.h>, and some toolchains want -lm at link time.
Where float runs out of significand bits
Above 2^24 consecutive integers no longer fit in a float, while a double still holds every integer up to 2^53.
<float.h>
<stdio.h>
int main(void)
{
float a = 16777216.0f; /* 2^24 */
float b = a + 1.0f;
double c = 16777216.0;
double d = c + 1.0;
printf("float : %.1f then %.1f\n", a, b);
printf("double : %.1f then %.1f\n", c, d);
printf("FLT_DIG = %d, DBL_DIG = %d\n", FLT_DIG, DBL_DIG);
return 0;
}
Example explained
Line 1A float has 24 significand bits, so beyond 2^24 the spacing between neighbouring floats is 2.0 and the odd integer 16777217 does not exist at all.
Line 2Assigning to float b forces the sum back to float precision, so the +1 is rounded away and b prints the same as a.
Line 3The same addition in double is exact, because 53 significand bits cover every integer up to 2^53.
Line 4FLT_DIG and DBL_DIG from <float.h> give the decimal digits that survive a decimal-to-binary-to-decimal round trip: 6 and 15.
Important notes
Not everything is inexact: integers up to 2^53 and fractions like 0.5, 0.25 and 0.125 are stored exactly, so == on such values is genuinely reliable.
The last bit of an expression can differ between compilers, optimisation levels, or 32-bit x87 code that keeps intermediates in wider precision, and -ffast-math permits reassociation; do not build logic around one specific bit pattern.
Common mistakes
Writing for (double t = 0.0; t != 1.0; t += 0.1): t goes 0.99999999999999989 then 1.0999999999999999 and never equals 1.0, and once t grows large enough that adding 0.1 no longer changes it the loop never ends.
Trusting printf("%f", 0.1 + 0.2), which prints 0.300000, and concluding the value is exact; the missing bits reappear later as an == that is always false or as visible drift after many additions.
Changing float to double to "fix" 0.1: the value stays inexact and the error only shrinks from about 1.5e-9 to about 5.6e-18, so every equality test still fails.
Try it yourself
Change, predict, then run
Print 0.1, 0.2, 0.3 and 0.1 + 0.2 with %.20f and note where the digits diverge. Then find two decimal literals other than 0.5 and 0.25 whose sum compares equal with ==, and state what their denominators have in common.
Open the C workspaceCheck your understanding
In C, 0.1 + 0.2 == 0.3 is false while 0.5 + 0.25 == 0.75 is true. What explains the difference?
- double keeps 15 decimal digits, so the 16th digit of 0.3 is lost, while 0.5 and 0.25 are short enough to survive
- 0.1 and 0.2 are float literals, whereas 0.5, 0.25 and 0.75 are double literals
- 1/10 and 3/10 repeat forever in base 2, so each literal becomes a nearby double and the rounded sum lands one step above the double stored for 0.3, while 1/2, 1/4 and 3/4 have power-of-two denominators and are stored exactly
- The addition is carried out in wider CPU registers, and the extra bits make the sum slightly too large
Show answer
The failure comes from base 2, not from a digit budget: 0.1, 0.2 and 0.3 each become their nearest double, and the addition is rounded a second time, ending one ulp (2^-54) above the double held by the literal 0.3. The "15 decimal digits" option is tempting but wrong, because 0.1 has a single decimal digit and is already inexact, while 0.5 and 0.25 are exact for the structural reason that their denominators are powers of two.