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
115: Floating-Point Limits with float.h
One of the most frustrating things for a developer moving into systems programming is realizing that floating-point numbers aren't actually numbers—they're approximations. I can't tell you how many times I've seen a junior dev spend an entire afternoon debugging a loop that never terminates because they were checking if a double exactly equaled 1.0. In the real world, 1.0 might actually be 1.0000000000000002, and your program just keeps spinning.
The naive way to handle this is to invent a "magic number" for your tolerance. You'll see a lot of code that looks like this:
if (fabs(a - b) < 0.00001) {
// Treat them as equal
}
The fragility of hardcoded tolerances
This looks fine on the surface, but it's a ticking time bomb. The problem is that 0.00001 is an arbitrary choice. If you're calculating the distance between two galaxies in meters, 0.00001 is impossibly strict; your check will almost always fail. If you're calculating the thickness of a microscopic cell membrane, 0.00001 is massive, and your check will treat two completely different values as identical.
You're basically guessing where the precision of the hardware ends and the "noise" begins. I've seen this cause catastrophic failures in embedded systems where the code worked in the simulator but failed on the actual hardware because the precision characteristics of the target CPU were slightly different.
Tuning to the machine with float.h
Instead of guessing, we use <float.h>. This header tells us exactly what the hardware is capable of. The most important tool here is DBL_EPSILON (or FLT_EPSILON for floats). Epsilon represents the difference between 1.0 and the next representable value of that type. It is the smallest possible "step" the machine can take at that magnitude.
The better way to compare two numbers is to scale that epsilon based on the magnitude of the numbers you're comparing. It looks more like this:
#include <float.h>
#include <math.h>
int are_nearly_equal(double a, double b) {
double diff = fabs(a - b);
a = fabs(a);
b = fabs(b);
double largest = (b > a) ? b : a;
if (diff <= largest * DBL_EPSILON) {
return 1;
}
return 0;
}
By multiplying DBL_EPSILON by the larger of the two numbers, we create a sliding window of tolerance. As the numbers get larger, the tolerance grows; as they get smaller, it shrinks. We're no longer guessing; we're letting the C standard and the hardware define the limit of precision.
Avoiding the "Infinity" Trap
Beyond precision, there's the issue of limits. I often see people use 1e30 or some other massive number to represent "infinity" or a "maximum possible value" when initializing a variable for a minimum-search algorithm. The problem is that 1e30 might not actually be the maximum. If your data happens to exceed that, your logic breaks.
float.h provides FLT_MAX and DBL_MAX. These aren't just big numbers; they are the absolute ceiling of what the type can hold. Using DBL_MAX tells anyone reading your code, "I am using the absolute limit of this architecture." It also prevents bugs where you might accidentally overflow your "magic infinity" if you perform a calculation on it.
Conversely, FLT_MIN is a bit of a trip. It's not the most negative number (that would be -FLT_MAX); it's the smallest positive normalized value. If you're writing a routine to prevent division by zero, checking if a value is less than FLT_MIN is a much safer bet than checking if it's exactly 0.0, because it catches values that are so small they're effectively zero for any practical calculation.
📋 Practical Task
Build a Magnitude-Aware Range Validator
Write a program that simulates a sensor reading system. You need to implement a function called is_within_tolerance that takes two double values (the measured value and the target value) and returns 1 if they are "equal enough" and 0 otherwise.
Your implementation must satisfy these requirements:
- Use
DBL_EPSILONfrom<float.h>to calculate the tolerance. - The tolerance must be scaled by the magnitude of the inputs (the larger of the two absolute values).
- Include a check at the start of your program to print the actual value of
DBL_MAXandDBL_EPSILONto the console so you can see the hardware limits you are working with. - Test your function with two sets of data: one set of very large numbers (e.g., 1 trillion) and one set of very small numbers (e.g., 1e-12) to prove that your scaled epsilon works where a hardcoded
0.00001would fail.
There are no comments for now.