Unveiling Floating-Point Numbers: The Secrets and Pitfalls of Decimals in C Language

Unveiling Floating-Point Numbers: The Secrets and Pitfalls of Decimals in C Language

In the last lesson, we explored in detail the int and its unsigned type unsigned int, mastering how integers are represented in computers. However, the numerical world in our lives is far more complex than just integers; our bank account balances and measurement results in scientific calculations often come with decimal points. So, how are these “decimals” represented and stored in C language? They are what we will delve into today,“floating-point numbers (Floating-point Number)”.

Many people intuitively think that floating-point numbers are simply small decimals. Indeed, conceptually, you can understand floating-point numbers as real numbers with a decimal part. For example,3.14, 2.75, and even scientific notation like 2.1e-8 all fall under the category of floating-point numbers. However, the mechanism by which computers store floating-point numbers is far more complex and sophisticated than we might imagine, and it also hides some important “traps”!

01

The “Lego Block” Principle of Floating-Point Numbers: The Wisdom of Decomposed Storage

In the world of computers, data storage is not as straightforward as we might think. For floating-point numbers, computers use a “divide and conquer” strategy. Simply put, it splits a floating-point number into two main parts for storage:the decimal part (also known as the mantissa) and the exponent part.

Imagine we are playing with Lego blocks. If we want to store a number, say3.14159, the computer does not simply put 3.14159 in as a whole. It’s more like:

Storing the “number body”: removing the decimal point from this number gives us an integer314159, which corresponds to the “significant digits” or “mantissa” of our floating-point number.

Storing the “position of the decimal point”: using a “flag” to mark where the decimal point should be placed. For example, for 3.14159, the flag is placed between 3 and 1.

This “flag” position information is determined by the “exponent”. In mathematics, we can move the decimal point by multiplying or dividing by powers of 10:

Moving one place to the right is equivalent to multiplying by10.

Moving one place to the left is equivalent to dividing by10 (or multiplying by 10 to the power of negative one).

Thus,3.14159 can be represented as 314159 * 10^-5. In the computer, it will store this “mantissa” 314159 and a value representing the “exponent” -5 separately. Therefore, when the computer needs to reconstruct this floating-point number, it can rebuild the original 3.14159 based on these two parts.

02

The “True Nature” of Floating-Point Numbers: Approximate Values and Binary Storage

Although this decomposition storage method is very clever, it leads to a crucial characteristic of floating-point numbers: floating-point numbers are usually just approximations of the actual values, not exact values.

Why is this the case?

Professional Correction and Supplement:

In the explanation, it was mentioned that “computers represent floating-point numbers by dividing them into decimal and exponent parts,” and a decimal example (like 3.14159 = 314159 * 10^-5) was used for analogy. This analogy is very helpful for understanding the structure of floating-point numbers. However, actual computers do not directly use decimal powers of 10 for low-level storage of floating-point numbers; instead, they usebinary representation.

According to the IEEE 754 standard, floating-point numbers are typically represented as:

Unveiling Floating-Point Numbers: The Secrets and Pitfalls of Decimals in C Language

This means that both the mantissa and the exponent will ultimately be converted to binary for storage. Some decimal fractions, such as0.1, are infinitely repeating in binary, just like the decimal representation of 1/3 (0.333…). Due to storage space limitations, computers can only truncate a finite number of binary digits for representation, leading toloss of precision.

For example: the simple integer 7.0, when stored as a floating-point number, might be stored as 6.9999999999999999 or 7.0000000000000001, which are extremely close but not exactly equal values.

03

Money does not “float”: The Application Traps of Floating-Point Numbers

Because floating-point numbers are approximations, their application in certain scenarios must be approached with caution. The most typical example is the storage and calculation of money.

Deepening Understanding and Supplement:

If bank account balances are stored using floating-point numbers (for example, using float or double types in C language), then 7.00 yuan might be stored as 6.9999999999999999 yuan. Imagine if every transaction had this tiny precision loss; over time, the bank’s accounts would become completely chaotic, with disastrous consequences!

Therefore, in fields such as finance and accounting, where high-precision calculations are required, it is common to avoid using floating-point numbers directly. Common solutions include:

Using integer types to store the “smallest unit” of currency: for example, converting all amounts to “cents” or “li” for storage, allowing the use of integer types (like long long) for precise calculations.

Using specialized fixed-point or high-precision calculation libraries: these data types and libraries can handle decimal calculations with arbitrary precision.

Thus, when we emphasize “your money should not be represented by floating-point numbers,” it is indeed a golden rule in programming practice!

03

Floating-Point Types in C Language

In C language, we mainly have two basic floating-point types:

float: single-precision floating-point number, occupying 4 bytes (32 bits). It can represent a relatively small range of values with lower precision, typically ensuring about 6-7 significant digits.

double: double-precision floating-point number, occupying 8 bytes (64 bits). It can represent a larger range of values with higher precision, typically ensuring about 15-17 significant digits. In most cases, if you need to handle decimals, double is the recommended choice as it provides higher precision.

Code Example:

#include <stdio.h>int main() {    float pi_float = 3.1415926535f; // Note that float constants need to have f or F suffix    double pi_double = 3.1415926535; // double constants default without suffix    printf("Single-precision float PI: %.10f\n", pi_float);    printf("Double-precision double PI: %.18lf\n", pi_double); // Output double using %lf    // The trap of floating-point comparison    double num1 = 0.1 + 0.2;    double num2 = 0.3;    printf("0.1 + 0.2 = %.18lf\n", num1);    printf("0.3 = %.18lf\n", num2);    if (num1 == num2) {        printf("num1 is equal to num2 (This might not always be true!)\n");    } else {        printf("num1 is NOT equal to num2 (Due to precision issues!)\n");    }    // Correct way to compare floating-point numbers: use a small tolerance (epsilon)    double epsilon = 0.00000001;    if ((num1 - num2 < epsilon) && (num2 - num1 < epsilon)) {        printf("num1 is approximately equal to num2 (Good practice for float comparison)\n");    }    return 0;}

Running the above code, you may find that the result of the comparison num1 == num2 is num1 is NOT equal to num2, which is a direct manifestation of the floating-point precision issue! Therefore, in C language, you should NEVER directly use == or != to compare two floating-point numbers, but rather compare whether their difference is within an acceptable small range.

04

Summary:

Through today’s learning, we not only understood how floating-point numbers are “decomposed” for storage in computers, but more importantly, recognized their essence as “approximate values” and the potential precision issues that arise from this.

  • Floating-point numbers are the computer representation of decimals and real numbers.

  • Computers split floating-point numbers into mantissa (significant digits) and exponent parts for storage.

  • Low-level storage is based on binary, which leads to some decimal fractions being unable to be represented accurately and only stored as approximations.

  • Precision issues make floating-point numbers unsuitable for scenarios requiring precise calculations, such as finance.

  • In C language, float provides single precision, while double provides double precision (more recommended).

  • Remember: Never directly compare two floating-point numbers for equality!

Unveiling Floating-Point Numbers: The Secrets and Pitfalls of Decimals in C Language

Leave a Comment