C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
10: Floating-Point Types and Precision
I want to show you a snippet of code that looks perfectly logical. Imagine you're writing a simple loop to track a progress bar or a timer that increments by 0.1 until it hits 1.0. You'd probably write something like this:
#include <stdio.h>
int main() {
float progress = 0.0f;
while (progress != 1.0f) {
printf("Progress: %f\n", progress);
progress += 0.1f;
}
printf("Reached 1.0!\n");
return 0;
}
If you run this, you'll notice something unsettling: it never stops. It prints "Progress: 0.900000" and then just keeps sailing right past 1.0, heading toward infinity. You'll be tempted to check your loop logic or your brackets, but the logic is sound. The problem is that the computer is lying to you—or rather, it's doing the best it can with a limited set of tools.
The Binary Fraction Trap
Here is the deal with floating-point numbers: they aren't stored as decimals. They are stored in binary (base-2). In base-10, we can't represent 1/3 exactly (it's 0.333... forever). Similarly, in base-2, a simple number like 0.1 cannot be represented exactly. It becomes a repeating fraction in binary.
When you write 0.1f, the compiler stores the closest possible approximation. Every time you perform progress += 0.1f, you aren't adding exactly one-tenth; you're adding a value slightly off from one-tenth. By the time you get near 1.0, those tiny errors have accumulated. The value of progress might actually be 1.00000012 or 0.9999998. Because it's not exactly 1.00000000, the != operator returns true, and your loop keeps running.
Comparing with an Epsilon
As a rule of thumb: never use == or != with floating-point numbers. Just don't do it. It's a recipe for bugs that are a nightmare to debug because they might work on one compiler but fail on another.
Instead, we use something called an "epsilon." An epsilon is just a fancy name for a very small number that represents the "tolerance" you're willing to accept. Instead of asking "Is this exactly 1.0?", we ask "Is the difference between this value and 1.0 smaller than my tolerance?"
#include <stdio.h>
#include <math.h> // Required for fabsf()
int main() {
float progress = 0.0f;
float epsilon = 0.00001f;
// Check if the absolute difference is greater than our tolerance
while (fabsf(progress - 1.0f) > epsilon) {
printf("Progress: %f\n", progress);
progress += 0.1f;
}
printf("Reached (approximately) 1.0!\n");
return 0;
}
I used fabsf() here, which returns the absolute value of a float. This ensures that whether progress is slightly above or slightly below 1.0, the difference is always positive, and the comparison works.
Float vs. Double: When to Care
You've probably noticed I used float and the f suffix (like 0.1f). In C, a float is typically 32 bits, while a double is 64 bits. A double has much more precision—roughly 15-17 significant decimal digits compared to the 6-9 you get with a float.
In the modern era, memory is cheap. If you aren't working on an embedded microcontroller with a tiny amount of RAM or processing millions of particles in a physics engine, just use double. It reduces the accumulation of rounding errors and saves you from a lot of headaches. If you switch to double, remember to use fabs() instead of fabsf() and drop the f suffixes from your literals.
📋 Practical Task
Build a Precision-Safe Budget Tracker
Create a program that simulates a bank balance. The program should start with a balance of 100.00. Use a loop to subtract a specific "subscription fee" of 0.10 repeatedly until the balance reaches exactly 0.00.
Requirements:
- Use
doublefor all monetary values to maintain higher precision. - Implement the loop termination using an epsilon value (e.g.,
0.0001) rather than a direct equality check. - Print the balance at every step, formatted to two decimal places using
%.2f. - Once the loop terminates, print a final message confirming the account is empty.
There are no comments for now.