Module 2 · Variables and Data Types
Floating Point: float, double, and Why 0.1 Is Not 0.1
In this lesson
- Choose
floatordoubleon purpose, and say why this track always picksdouble. - Explain in one paragraph why
0.1 + 0.2is not0.3. - Compare two decimal numbers with a tolerance instead of
==.
Maria's shop till adds three prices and prints a total that is one paisa short. She checks it on paper four times. The paper is right and the machine is right, and they disagree.
Nobody made a mistake. A decimal number does not fit in a binary box, any more than one third fits in a decimal one. This lesson is about living with that.
A decimal number in a binary box
Write one third in decimal and you get 0.333333, for as long as you are willing to keep writing. Stop anywhere and you have written something that is not one third.
A computer has the same problem, in base two. Its digits are halves, quarters, eighths and sixteenths, and no sum of those ever lands exactly on one tenth.
So when you write 0.1 in C, the machine stores the nearest value it can build. That value is close, and it is not 0.1.
#include <stdio.h>
int main(void)
{
double a = 0.1;
double b = 0.2;
printf("0.1 is stored as : %.20f\n", a);
printf("0.2 is stored as : %.20f\n", b);
printf("0.3 is stored as : %.20f\n", 0.3);
return 0;
}
0.1 is stored as : 0.10000000000000000555
0.2 is stored as : 0.20000000000000001110
0.3 is stored as : 0.29999999999999998890
%.20f asks for twenty places after the point, which is how you see what is really there. Every other program you have written printed six places and hid all of this.
So a decimal literal in your source is a request, and what you get is the nearest value the machine can build.
float and double: how many digits each one keeps
C gives you two sizes of decimal box that you will actually use.
| Type | Size | Digits it keeps correctly | Largest value | printf |
|---|---|---|---|---|
float | 4 bytes | about 7 | about 3.4 times 10 to the 38 | %f |
double | 8 bytes | about 15 or 16 | about 1.7 times 10 to the 308 | %f |
long double | 16 bytes here | 18 or more | larger still | %Lf |
"Digits it keeps correctly" is the number that matters, and you can watch it run out.
#include <stdio.h>
int main(void)
{
float as_float = 3.141592653589793f;
double as_double = 3.141592653589793;
printf("float : %.15f\n", as_float);
printf("double: %.15f\n", as_double);
return 0;
}
float : 3.141592741012573
double: 3.141592653589793
Compare the two lines digit by digit. They agree for seven digits, and after that the float is inventing.
Use double by default. It is not slower on any processor you will use, and 4 saved bytes are never worth eight lost digits.
Notice the f on the end of the first literal. Without it the number is a double that then gets squeezed into a float, which is the same answer by a longer road.
The 0.1 plus 0.2 problem, printed to twenty places
This is the program every C learner should run exactly once, and then remember for ever.
#include <stdio.h>
int main(void)
{
double a = 0.1;
double b = 0.2;
double sum = a + b;
printf("0.1 + 0.2 = %.20f\n", sum);
printf("0.3 = %.20f\n", 0.3);
printf("are they the same? %d\n", sum == 0.3);
return 0;
}
0.1 + 0.2 = 0.30000000000000004441
0.3 = 0.29999999999999998890
are they the same? 0
Two values that are near 0.3 were added, and the answer landed on a different fence post from the one that 0.3 itself lands on.
The last line shows something else worth having early. A comparison in C is an expression, and its value is 1 or 0. You can print it, exactly as we just did.
So == on two decimal numbers asks whether two fence posts are the same post. That is almost never the question you meant to ask.
Comparing with a tolerance
The question you meant was "are these close enough to be the same". So ask that.
fabs, from <math.h>, gives the size of a difference without its sign. Compare that size against a small number, often called epsilon.
#include <stdio.h>
#include <math.h>
int main(void)
{
double sum = 0.1 + 0.2;
double epsilon = 1e-9;
printf("the difference : %.20f\n", fabs(sum - 0.3));
printf("close enough? : %d\n", fabs(sum - 0.3) < epsilon);
return 0;
}
the difference : 0.00000000000000005551
close enough? : 1
1e-9 is scientific notation for 0.000000001. It is a reasonable epsilon for ordinary numbers and a bad one for very large or very small ones.
Some systems need -lm on the command line to link the maths library. The Playground already passes it, so fabs works there with nothing added.
So the rule is: never == between two decimal numbers, always a difference against a tolerance you chose on purpose.
Integer division, and the trap in an average
Divide two whole numbers in C and you get a whole number. The fraction is not rounded; it is thrown away.
#include <stdio.h>
int main(void)
{
int a = 7;
int b = 2;
printf("int / int : %d\n", a / b);
printf("stored in a double: %f\n", (double)(a / b));
printf("done as decimals : %f\n", a / (double)b);
return 0;
}
int / int : 3
stored in a double: 3.000000
done as decimals : 3.500000
The middle line is the one that catches people. The variable is a double, and the value it holds was already ruined before it got there.
The fix goes on the division, not on the result. Make one side a decimal type and the whole division is done in decimals.
So (a + b + c) / 3 is a whole number, and (a + b + c) / 3.0 is not. One character.
Printing: %f, %.2f, %e and %g
A double has one value and many ways to show it.
Four ways to print the same number
double v = 1234.5678;
printf("%f", v) 1234.567800 six places, always
printf("%.2f", v) 1234.57 two places, rounded
printf("%e", v) 1.234568e+03 scientific notation
printf("%g", v) 1234.57 short form, picks for you
%fprints six places whether or not they mean anything.%.2fis the one a judge usually asks for. The number after the dot is the count of places.%esuits a value whose size you do not know in advance.- In
scanf, adoubleneeds%lf, not%f. This catches everybody once.
Printing rounds the value that is stored, not the text you typed. That is why %.2f of 1.005 is 1.00, as the next example shows.
When not to use a decimal type at all
Money and counts are not decimal numbers. They are whole numbers of the smallest unit, and they should be stored that way.
A bank stores paisa, not taka. A shop stores paisa. An invoicing system stores cents. Every addition is then exact, and the decimal point is put back only when the number is printed.
So the rule that follows every professional programmer around: floating point is for measuring, integers are for counting. Money is counting.
One line of arithmetic, printed twice. Six places hide the problem and twenty show it.
#include <stdio.h>
int main(void)
{
double third = 1.0 / 3.0;
printf("six places : %f\n", third);
printf("twenty places: %.20f\n", third);
return 0;
}
six places : 0.333333
twenty places: 0.33333333333333331483
The second line stops being 3 after about sixteen digits. That is the "about 15 or 16 digits" from the table, seen directly.
Run in CompilerTwo numbers that should be equal, compared both ways in one program.
#include <stdio.h>
#include <math.h>
int main(void)
{
double sum = 0.1 + 0.2;
double target = 0.3;
double epsilon = 1e-9;
printf("with == : %d\n", sum == target);
printf("with tolerance : %d\n", fabs(sum - target) < epsilon);
printf("the difference : %.20f\n", fabs(sum - target));
return 0;
}
with == : 0
with tolerance : 1
the difference : 0.00000000000000005551
The difference is about five hundredths of a millionth of a millionth. Epsilon is a billionth, which is far bigger, so the second answer is 1.
Run in CompilerA bill line of 2.675 taka. Once as a double, once as whole paisa in a long long.
#include <stdio.h>
int main(void)
{
double taka = 2.675;
long long paisa = 268; /* the same price, rounded to paisa first */
printf("as a double, 2 places : %.2f\n", taka);
printf("as a double, 20 places: %.20f\n", taka);
printf("as whole paisa : %lld.%02lld\n", paisa / 100, paisa - (paisa / 100) * 100);
return 0;
}
as a double, 2 places : 2.67
as a double, 20 places: 2.67499999999999982236
as whole paisa : 2.68
The till printed 2.67 and the customer expected 2.68. Nothing rounded badly: the stored value really is below 2.675, so two places is 2.67.
The third line is the professional answer. Round to paisa once, at the edge of the system, then do every sum in whole paisa where nothing can drift.
Run in CompilerWhere this is used
- A game's physics loop. Unreal and Godot both store positions and velocities as
float, because a frame needs millions of them and 7 digits is plenty for a metre. Large open worlds hit the limit and switch todouble. - GPS coordinates. A latitude as a
floatis accurate to roughly a metre; as adoubleit is accurate to far less than a millimetre. Mapping software usesdoublefor exactly this reason. - Audio. A sample in a WAV file is a 16-bit integer. Every mixing and filtering step inside an audio engine is done in
float, then converted back on the way out. - Banking. Core banking systems and the Stripe API both represent an amount as a whole number of the smallest currency unit. Stripe's API takes 1099 and means 10.99, in an integer field.
- Patriot missile battery, Dhahran, 1991. The system counted time in tenths of a second, stored in a 24-bit fixed-point value that could not hold one tenth exactly. After 100 hours running the clock had drifted by about a third of a second, which is 600 metres of missile.
Common mistakes
1. Printing a decimal value with %d.
double price = 3.5;
printf("%d\n", price);
The Playground's GCC 12 says nothing, because the format check lives behind -Wall. It printed 0 on our run, and there is no promise it prints the same for you. A local gcc -Wall says warning: format '%d' expects argument of type 'int', but argument 2 has type 'double'. Use %f.
2. Reading a double with %f.
double d = 0.0;
scanf("%f", &d);
printf("%f\n", d);
Silent on the Playground; it printed 0.000000 for the input 3.5. scanf was told to write 4 bytes into an 8-byte box. In printf a float is promoted to a double, so %f serves both; in scanf nothing is promoted, so a double needs %lf.
3. Dividing two integers and hoping for a fraction.
int total = 7;
int count = 2;
double average = total / count;
printf("%f\n", average);
No message anywhere. You expect 3.500000 and it prints 3.000000. The division finished as whole-number work before the double ever saw the answer. Write total / (double)count.
4. Testing two decimal values with ==.
double sum = 0.1 + 0.2;
printf("%d\n", sum == 0.3);
No message, and it prints 0. The two values differ in the seventeenth decimal place, and == asks about all of them. Compare a difference against an epsilon instead.
Zara is cutting circular table mats and needs the area of each one.
Input. One line with one decimal number r, the radius in centimetres.
Output. One line with the area, to exactly two decimal places.
Constraints. 0.01 <= r <= 1000.00. Use 3.14159265358979 for pi.
Sample. Input 2.5 gives 19.63.
#include <stdio.h>
int main(void)
{
double r = 0.0;
scanf("%lf", &r);
/* One printf, with %.2f. */
return 0;
}
Graded as circle-area. The %lf in the scanf line is deliberate; changing it to %f makes every test fail at once.
Finish Exercise 3 from lesson 1, now that you know why it was hard. Three integer marks, one average, two decimal places.
Input. One line with three integers a b c.
Output. One line with their average, to two decimal places.
Constraints. 0 <= a, b, c <= 100.
Sample. Input 1 1 2 gives 1.33.
#include <stdio.h>
int main(void)
{
int a = 0;
int b = 0;
int c = 0;
scanf("%d %d %d", &a, &b, &c);
/* Change one character of (a + b + c) / 3 and it works. */
printf("%.2f\n", 0.0);
return 0;
}
Not graded in this module. Try both / 3 and / 3.0 with the input 1 1 2 and keep the two outputs side by side.
Amara's weather page shows temperatures in both scales. Convert one Celsius reading to Fahrenheit.
Input. One line with one decimal number c.
Output. One line with the Fahrenheit value, to exactly one decimal place.
Constraints. -100.0 <= c <= 100.0. Fahrenheit is c times 9 divided by 5, plus 32.
Sample. Input 37.0 gives 98.6.
#include <stdio.h>
int main(void)
{
double c = 0.0;
scanf("%lf", &c);
/* Write 9.0 and 5.0, not 9 and 5, and say why to yourself. */
printf("%.1f\n", 0.0);
return 0;
}
Not graded in this module. Writing c * 9 / 5 happens to work here, because c is already a double. Writing 9 / 5 * c does not, and finding out why is the exercise.
Build the comparison this lesson has been arguing for. Read three decimal numbers and report whether the first two add up to the third.
Input. One line with three decimal numbers a b c.
Output. One line: 1 if a + b is within 0.000000001 of c, otherwise 0.
Constraints. -1000000 <= a, b, c <= 1000000. One hidden test is 0.1 0.2 0.3, and the expected answer there is 1.
Sample. Input 0.1 0.2 0.3 gives 1. Input 1.0 1.0 3.0 gives 0.
#include <stdio.h>
#include <math.h>
int main(void)
{
double a = 0.0;
double b = 0.0;
double c = 0.0;
scanf("%lf %lf %lf", &a, &b, &c);
/* One printf with %d. A comparison is already a 1 or a 0. */
return 0;
}
Graded as close-enough. Writing a + b == c passes the second sample and fails the first, which is the whole point of the problem.
Common doubts
Is this a bug in C?
No. Python, Java, JavaScript and your phone's calculator all do the same thing, because they all use the same IEEE 754 format. C is simply the language that lets you look at it.
What should epsilon be?
1e-9is a fine default for numbers near 1. For very large numbers the gap between neighbours is itself large, so a fixed epsilon stops working and you compare a relative difference instead.Why does
printftake%ffor adoublebutscanftake%lf?A
floathanded toprintfis widened to adoubleautomatically, so one specifier covers both.scanfreceives an address and must be told the exact size to write.Can I store money in a
doubleif I always print with%.2f?For a school exercise, yes. For anything real, no: the errors accumulate across many additions, and one day a total is off by a paisa that nobody can find.
Which decimal numbers are stored exactly?
Those whose fraction is a sum of halves: 0.5, 0.25, 0.75, 0.125 and so on. Every whole number up to about 9 quadrillion is exact in a
doubletoo.
Key takeaways
- A decimal literal is stored as the nearest binary value, which is usually not the value you wrote.
doublekeeps about 15 digits andfloatabout 7; this track usesdoubleeverywhere.0.1 + 0.2 == 0.3is 0, and printing at%.20fshows you exactly why.- Compare with
fabs(a - b) < epsilon, never with==. - Whole number divided by whole number is a whole number, however wide the box you store it in.
- Money and counts are integers of the smallest unit; decimals are for measuring.
Next are the two types that pretend not to be numbers, and the reason a comparison can be added to another comparison.
End of lesson 3
Mark it done, and your progress moves with you.
Next: char, bool, and the Types That Are Really Numbers