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
200: Common Undefined Behavior Pitfalls
I once spent an entire weekend chasing a bug that only existed in the production build. In the debug environment, the code worked perfectly. But the moment I turned on -O3 optimization, a critical if statement—one designed to catch null pointers—simply disappeared. The compiler hadn't missed it; it had deliberately deleted it. Why? Because earlier in the function, I had dereferenced that same pointer. To the compiler, dereferencing a null pointer is Undefined Behavior (UB). The C standard essentially says, "This will never happen in a valid program." Therefore, the compiler reasoned that since the pointer was dereferenced, it couldn't be null, making the subsequent null check redundant and safe to remove. It's a jarring realization: UB isn't just a crash; it's a permission slip for the compiler to rewrite your logic.
The Optimizer's License to Lie
When you see "Undefined Behavior" in the manual, don't read it as "the program might crash." Read it as "the compiler is no longer obligated to make sense." Modern compilers like GCC and Clang don't just translate your code; they analyze it for mathematical impossibilities. If you write code that triggers UB, you are effectively telling the compiler, "I promise I will never do this." If you then actually do it, the compiler is free to assume that path of execution is unreachable.
One of the most insidious examples is signed integer overflow. In many languages, if a signed 32-bit integer hits 2,147,483,647 and you add 1, it wraps around to a negative number. In C, signed overflow is UB. I've seen cases where a loop like for (int i = 0; i <= limit + 1; i++) was optimized into an infinite loop because the compiler assumed i would never overflow and therefore the termination condition would always eventually be met, regardless of the actual hardware behavior. If you need wrap-around behavior, you must use unsigned int.
The Memory Minefield
You've already learned about pointers, but UB is where those pointers become dangerous. The most common trap is the "dangling pointer"—using memory after you've called free(). Now, if you're lucky, the program crashes immediately with a Segmentation Fault. If you're unlucky, it continues to work. This is the "Heisenbug" scenario: the memory hasn't been reclaimed by the OS yet, so your pointer still points to the old value. You think your code is correct, but the moment you deploy it to a system with a different memory allocator or more load, that memory is overwritten by another thread, and your data vanishes.
Then there is the classic off-by-one error. Accessing array[10] when the array only has 10 elements (indices 0-9) is UB. You might just be reading a piece of a neighboring variable, or you might be overwriting the return address on the stack. I've seen a bug where an off-by-one error in a string buffer didn't crash the program, but it subtly changed the value of a boolean flag located immediately after the buffer in memory, flipping the application's "admin mode" to true. That's not just a bug; that's a security vulnerability.
The Fragility of String Literals
A final point of caution: never try to modify a string literal. Writing char *s = "Hello"; s[0] = 'h'; looks innocent, but in most modern environments, string literals are stored in a read-only section of the binary. Attempting to modify them is UB. On some systems, it will trigger a hardware exception (a crash). On others, if the linker happened to place the string in a writable section, it might actually work. This inconsistency is exactly why UB is so dangerous; it creates a false sense of security that disappears the moment you change compilers or target a new architecture.
📋 Practical Task
Fixing the Corrupted Loop Counter
Below is a piece of code that exhibits a subtle form of Undefined Behavior. The developer is trying to process a buffer, but they've made a critical mistake regarding memory boundaries. When run on some compilers, this code might actually "work" or print strange values, but it is fundamentally broken.
#include <stdio.h> #include <stdlib.h> int main() { int size = 5; int *array = malloc(size * sizeof(int)); for (int i = 0; i < size; i++) { array[i] = i * 10; } // BUG: The developer intends to print the array, // but the loop condition is slightly off. for (int i = 0; i <= size; i++) { printf("Value at index %d: %d\n", i, array[i]); } free(array); return 0; }Your Task:
- Identify the exact line causing Undefined Behavior.
- Correct the code so it only accesses valid memory.
- Add a comment explaining why accessing
array[size]is UB and what the potential real-world consequences could be beyond a simple crash.
There are no comments for now.