C for People Who Already Know How to Code
Memory Model and Undefined Behavior
The layout your program actually gets
Before you can reason about a dangling pointer, it helps to know what's actually sitting in memory. A running process's address space is divided into a handful of regions, and where a value lives determines its whole lifetime story.
Two of these move: the heap grows as you malloc, the stack grows as functions call deeper. Everything else is fixed at load time. The gap between them is why a runaway recursive function and a runaway allocation loop eventually collide in the same crash: they're growing toward each other.
Stack vs heap, concretely
A local variable's storage is reclaimed the instant its function returns, whether or not anything still points at it. This is legal C that produces a dangling pointer:
int *make_counter(void) {
int counter = 0;
return &counter; // counter's storage is gone the moment this returns
}The returned address isn't null and the compiler won't stop you. It's a pointer to a stack slot that now belongs to whatever gets called next. Reading through it is undefined behavior, not "probably still zero." If you need the value to outlive the function, it has to live on the heap (malloc) or be passed in by the caller.
What "undefined behavior" actually means
It's tempting to read "undefined behavior" as "the result is unspecified, but reasonable." That's not what the standard says, and it's not how compilers treat it. The standard says a program that triggers UB has no meaning at all. Compilers use that as license to assume it never happens, then optimize as if the code that would trigger it is unreachable.
Signed integer overflow is the clearest example. It's UB, full stop, on every C compiler that matters. So a compiler is free to assume a signed loop counter never overflows. If your loop's termination depends on it overflowing, the compiler can delete the check:
// looks like it terminates when i wraps negative, but it might not.
for (int i = 0; i >= 0; i++) {
sum += arr[i];
}Try it yourself: watch a compiler exploit signed overflow UB›
Compile the loop above at -O0 and -O2 (or paste it into Compiler Explorer) and compare the generated assembly. At higher optimization the compiler may assume i >= 0 is always true and turn this into something that never checks the bound at all, because it's reasoned that a loop relying on overflow to terminate cannot legally exist.
Sequence points: the i = i++ + ++i trap
Modifying the same variable twice with no sequence point between the modifications is undefined, regardless of what "left to right" intuition tells you:
int i = 1;
i = i++ + ++i; // undefined behavior, not "some particular surprising number"There's no correct answer to print here. Different compilers, or the same compiler at different optimization levels, can legitimately produce different results. The fix isn't to memorize evaluation order, it's to never write more than one unsequenced modification to the same object in one expression.
Every category of UB worth keeping in your head
- Dereferencing a null, dangling, or uninitialized pointer.
- Signed integer overflow (unsigned overflow is well-defined wraparound, see the integers page).
- Reading or writing past the end of an array, including computing a pointer more than one-past-the-end, even if you never dereference it.
- Reading a value before it's initialized.
- Violating strict aliasing (below).
- A data race: two threads accessing the same object where at least one write isn't synchronized.
- Shifting by a negative amount or by ≥ the width of the type (
x << 32on a 32-bitint). - Calling a function through a pointer of an incompatible type.
Strict aliasing: the UB nobody warns you about
The compiler assumes two pointers of unrelated types never point at the same memory: that's the strict aliasing rule. It uses this assumption to reorder and cache reads/writes for speed. Casting a pointer to an unrelated type and dereferencing it breaks that assumption:
float f = 3.14f;
int *p = (int *)&f;
int bits = *p; // strict-aliasing violation: reading a float object through an int*The fix that's actually portable is memcpy into a same-sized destination. It gets optimized down to a plain load/store by any competent compiler, with none of the UB:
float f = 3.14f;
int bits;
memcpy(&bits, &f, sizeof bits);A union is the other common answer, and it's worth being precise here since C and C++ disagree: reading a union member other than the one most recently written is, strictly speaking, implementation-defined by the ISO C standard, but GCC and Clang both explicitly document it as supported, and it's the idiomatic way to type-pun in C. It is not legal in C++, despite looking identical.
Try it yourself: see a strict-aliasing violation get flagged›
gcc -O2 -Wall -Wextra -Wstrict-aliasing -c aliasing.cTry the raw-cast version above and the memcpy version, both at -O2. If your target has ever needed -fno-strict-aliasing to "fix" a bug, that flag didn't fix the bug. It just told the compiler to stop assuming the rule, at a real performance cost. The actual fix is removing the violation.