Floating-point numbers
Floating-point types store numbers with a fractional part, and they store them approximately. Almost every surprise with them comes from forgetting that sentence: the value in memory is the nearest binary fraction, not the decimal you typed. This lesson shows the precision, the traps and the tools to live with them.
float and double
float is the smaller type, double the default one for literals and library functions. The macros in float.h give the number of decimal digits each type can hold without losing information and the largest value it can store. Use double unless memory or a file format demands otherwise.
#include <float.h>
#include <stdio.h>
int main(void)
{
printf("float: %zu bytes, %d digits\n", sizeof(float), FLT_DIG);
printf("double: %zu bytes, %d digits\n", sizeof(double), DBL_DIG);
printf("FLT_MAX %e\n", FLT_MAX);
printf("DBL_MAX %e\n", DBL_MAX);
printf("FLT_EPSILON %.9g\n", FLT_EPSILON);
return 0;
}
gcc -Wall -Wextra types.c -o types
./types
float: 4 bytes, 6 digits
double: 8 bytes, 15 digits
FLT_MAX 3.402823e+38
DBL_MAX 1.797693e+308
FLT_EPSILON 1.1920929e-07
Six digits for float is why prices and counters should not use it: a value around a million already loses units. The epsilon is the gap between 1 and the next representable float.
0.1 is not 0.1
A double is a sign, an exponent and a binary fraction. One tenth has no finite binary expansion, just as one third has no finite decimal one, so the stored value is the nearest fraction. Adding two of them accumulates the error, and seventeen digits show it.
#include <stdio.h>
int main(void)
{
double a = 0.1;
double b = 0.2;
double sum = a + b;
printf("a %.17g\n", a);
printf("b %.17g\n", b);
printf("a + b %.17g\n", sum);
printf("0.3 %.17g\n", 0.3);
printf("equal? %d\n", sum == 0.3);
return 0;
}
gcc -Wall -Wextra precision.c -o precision
./precision
a 0.10000000000000001
b 0.20000000000000001
a + b 0.30000000000000004
0.3 0.29999999999999999
equal? 0
The two values differ in the last bits, so equality is false. This is not a compiler bug and not a reason to avoid floating point; it is the reason to compare with a tolerance.
Comparing floating point
Ask whether two values are closer than a tolerance instead of asking whether they are equal. fabs comes from math.h, and the maths library must be linked with -lm after the source files. The tolerance depends on the values: 1e-9 is fine for numbers around one.
#include <math.h>
#include <stdio.h>
int main(void)
{
double a = 0.1 + 0.2;
double b = 0.3;
printf("exact comparison: %d\n", a == b);
printf("within 1e-9: %d\n", fabs(a - b) < 1e-9);
return 0;
}
gcc -Wall -Wextra epsilon.c -o epsilon -lm
./epsilon
exact comparison: 0
within 1e-9: 1
For a relative comparison divide the difference by the larger magnitude, otherwise a tolerance that suits metres is meaningless for nanometres. Choose the rule from the problem, not from the type.
Formatting
%f prints fixed notation, %e scientific notation and %g chooses between them. A precision after the dot sets the number of digits, and a width pads the field, which is how reports line up.
#include <stdio.h>
int main(void)
{
double value = 1234.5678;
printf("f: %f\n", value);
printf("f: %.2f\n", value);
printf("e: %e\n", value);
printf("g: %g\n", value);
printf("width: |%10.2f|\n", value);
return 0;
}
gcc -Wall -Wextra format.c -o format
./format
f: 1234.567800
f: 1234.57
e: 1.234568e+03
g: 1234.57
width: | 1234.57|
%f defaults to six digits after the dot, not to the precision of the type. The last line pads to ten characters in total, including the dot and the two decimals.
Infinity and NaN
IEEE arithmetic defines results for operations that have no finite answer: dividing a positive number by zero gives infinity, and subtracting infinity from itself gives NaN. Both are valid double values, and isinf and isnan test for them. Integer division by zero is a different story: it is undefined behaviour.
#include <math.h>
#include <stdio.h>
int main(void)
{
double zero = 0.0;
double one = 1.0;
double inf = one / zero;
double nan = inf - inf;
printf("inf: %f, isinf: %d\n", inf, isinf(inf) != 0);
printf("nan: %f, isnan: %d\n", nan, isnan(nan) != 0);
return 0;
}
gcc -Wall -Wextra special.c -o special -lm
./special
inf: inf, isinf: 1
nan: -nan, isnan: 1
The minus sign in front of NaN is not meaningful: NaN carries a sign bit that the arithmetic happened to set, and every comparison with NaN is false, including nan == nan. Test with isnan, never with equality.
A double in an int slot
The conversion is wrong and the program still builds; only the warning points at it. When the format string is a literal, GCC compares each conversion with the argument type.
#include <stdio.h>
int main(void)
{
double ratio = 0.5;
printf("%d\n", ratio);
return 0;
}
gcc -Wall -Wextra fmt.c -o fmt
fmt.c: In function 'main':
fmt.c:7:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
7 | printf("%d\n", ratio);
| ~^ ~~~~~
| | |
| | double
| int %f
The last line suggests the conversion that matches the argument. A missing or wrong conversion in printf is not a small cosmetic issue: the function reads the argument according to the format, so the printed value is unrelated to the variable.
Exercises
- Compute the sum of 0.1 a thousand times and print it with %.17g. Compare with the exact result and explain the difference.
- Write a function that compares two doubles with a relative tolerance and test it with 1e-12 and 1e12.
- Print DBL_MAX * 2.0 and DBL_MAX - DBL_MAX. Which of the two is infinity and which is NaN?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
How many decimal digits can a
doublehold without losing information?Answer
15
DBL_DIGis 15 on this platform, while afloatkeeps about 6 digits. -
Why is the stored value of
0.1not exactly one tenth?Answer
One tenth has no finite binary expansion
Like one third in decimal, one tenth has no finite expansion in binary, so the nearest representable fraction is stored.
-
What is the correct way to compare two computed
doublevalues?Answer
By checking that their difference is below a tolerance
Rounding makes exact equality unreliable, so the difference is compared with a tolerance chosen for the problem.
-
How many digits after the decimal point does
%fprint by default?Answer
6
%fdefaults to six digits after the point; the precision of the type does not change that default. -
What does this program print?
#include <stdio.h> int main(void) { printf("%d\n", 0.1 + 0.2 == 0.3); return 0; }Answer
0
The sum is
0.30000000000000004, which differs from the stored0.3, so the comparison is false. -
What does this program print?
#include <stdio.h> int main(void) { printf("%.17g\n", 0.1 + 0.2); return 0; }Answer
0.30000000000000004
Seventeen significant digits show the actual sum, one bit above the nearest double to 0.3.
-
What does this program print?
#include <stdio.h> int main(void) { printf("[%10.2f]", 1234.5678); return 0; }Answer
[ 1234.57]
The field is ten characters wide and right-aligned, with two digits after the point.
-
What does this program print?
#include <stdio.h> int main(void) { printf("%f\n", 1.0 / 0.0); return 0; }Answer
inf
IEEE arithmetic defines positive division by zero as infinity, and
%fprints it asinf. -
How does integer division by zero differ from floating-point division by zero?
Answer
Integer division is undefined, float division gives infinity
Floating-point division by zero has a defined IEEE result, while integer division by zero is undefined behaviour.
-
What is wrong with testing a NaN result with
value == value?Answer
It is always false for NaN
NaN compares unequal to everything, including itself, so the test must use
isnan.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev