Learn C Programming

Lesson 3 of 7 · Variables and Data Types

Module 2 · Variables and Data Types

Floating Point: float, double, and Why 0.1 Is Not 0.1

FreeReading

In this lesson

  • Choose float or double on purpose, and say why this track always picks double.
  • Explain in one paragraph why 0.1 + 0.2 is not 0.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.

The doubles nearest to 0.1, and the gap between them The doubles that exist near one tenth a double 0.09999999999999999167 exact 0.1 no double lands here what C stores for 0.1 0.10000000000000000555 the next one up 0.10000000000000001943 The gap between two neighbouring doubles here is about 0.0000000000000000139. Every decimal between them has to pick one. 0.1 picks the one on the right.
Figure 1. The doubles near 0.1 are a row of fence posts. The exact value falls between two posts, so C stores the nearer post.

float and double: how many digits each one keeps

C gives you two sizes of decimal box that you will actually use.

TypeSizeDigits it keeps correctlyLargest valueprintf
float4 bytesabout 7about 3.4 times 10 to the 38%f
double8 bytesabout 15 or 16about 1.7 times 10 to the 308%f
long double16 bytes here18 or morelarger 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
  • %f prints six places whether or not they mean anything.
  • %.2f is the one a judge usually asks for. The number after the dot is the count of places.
  • %e suits a value whose size you do not know in advance.
  • In scanf, a double needs %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.

Example 1: the smallest surprise

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 Compiler
Example 2: the tolerance, written out

Two 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 Compiler
Example 3: Maria's till, the one a beginner actually writes

A 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 Compiler

Where 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 to double.
  • GPS coordinates. A latitude as a float is accurate to roughly a metre; as a double it is accurate to far less than a millimetre. Mapping software uses double for 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.

Brain teaser

Kenji stores a serial number in a float because "it is a number and floats hold big numbers".

#include <stdio.h>

int main(void)
{
    float serial = 16777217.0f;

    printf("%f\n", serial);
    return 0;
}

It prints 16777216.000000. The value is far below the largest float, and nothing overflowed. Say why the last digit was lost. Then answer the harder half: name the smallest whole number a float cannot hold, and say what 16777216 is in powers of two.

A float keeps about 7 decimal digits, which is about 24 binary ones. Count the binary digits in 16777217.

Exercise 1Easy

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.

Run in Compiler
Exercise 2Medium

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.

Run in Compiler
Exercise 3Medium

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.

Run in Compiler
Exercise 4Hard

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.

Run in Compiler

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-9 is 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 printf take %f for a double but scanf take %lf?

    A float handed to printf is widened to a double automatically, so one specifier covers both. scanf receives an address and must be told the exact size to write.

  • Can I store money in a double if 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 double too.

Key takeaways

  • A decimal literal is stored as the nearest binary value, which is usually not the value you wrote.
  • double keeps about 15 digits and float about 7; this track uses double everywhere.
  • 0.1 + 0.2 == 0.3 is 0, and printing at %.20f shows 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