A pointer that outlives what it pointed at
You have spent this whole rung learning what a pointer can do: hold an address, follow it with a single `*`, walk through an array with pointer arithmetic, and rest on the stack frame that a function builds when it is called. Every one of those tricks assumed one quiet thing — that the thing on the far end of the arrow is still there. This last guide is about what happens when it is not. A pointer is just a number; it has no idea whether the storage it names is alive, freed, or has quietly become someone else's. When that number outlives the thing it pointed at, you hold a dangling pointer: an address that was once a perfectly good house number and now points at a demolished lot.
There is a close cousin you already met in guide 2: the wild pointer. A wild pointer was never aimed anywhere on purpose — it is an uninitialized pointer still holding whatever leftover bits were sitting in that memory, a random house number it inherited by accident. The dangling pointer is more treacherous precisely because it once was correct. The wild pointer never had your trust; the dangling pointer earned it, used it, and then quietly lost the right to it. Both share one fatal property: the address looks every bit as ordinary as a valid one, so nothing in the number itself warns you that following it is now a mistake.
Two ways to make one: the runaway frame and the early free
The first classic way to manufacture a dangling pointer is to return the address of a local. Recall from guide 4 that a function's locals live in its stack frame, which is popped off the call stack the moment the function returns. So the instant a function hands back `&local`, the storage that address names is reclaimed; the very next function call will build its own frame right on top of it and overwrite the value. The pointer the caller holds now aims into reused stack space. It may seem to work — the old bytes might linger for a heartbeat — which is the cruelest part: the bug hides until some unrelated call stomps on those bytes, and then your data silently changes underfoot.
int *make_counter(void) {
int c = 0; /* c lives in THIS frame */
return &c; /* BUG: hands back a soon-dead address */
} /* frame popped here; c is gone */
int *p = make_counter(); /* p now dangles */
printf("%d\n", *p); /* use-after-return: UNDEFINED */The second way lives on the heap and is even more common in real code: the early free(). When you call free() on a block, you are telling the allocator "I am done; you may hand this region to someone else." Your pointer still holds the block's address, but the bytes there are now the allocator's to recycle. Reading or writing through that pointer afterward is use-after-free, and it is one of the most dangerous bugs in all of systems programming. At best it crashes; at worst the allocator has already handed those bytes to a fresh malloc() elsewhere, so your stale write quietly corrupts a totally unrelated object — and attackers build entire exploits on steering exactly this.
What a segmentation fault actually is
You have seen the phrase segmentation fault thrown at you several times now, usually as the punishment for dereferencing NULL or wandering out of bounds. Let us say plainly what it is, because the name hides a beautiful piece of machinery. Your program does not run on raw hardware addresses; it runs in its own private map of memory, and the operating system marks each region of that map with permissions — readable, writable, executable, or simply not mapped at all. This is memory protection, and it is enforced in hardware on every single memory access, at no cost to you.
A segmentation fault is what happens when your program touches an address in a way the map forbids: reading where nothing is mapped, or writing where only reading is allowed. The CPU detects the violation mid-instruction and traps into the kernel, which decides your process has broken its contract and delivers a signal that, by default, kills it on the spot — printing that terse "Segmentation fault (core dumped)" line. Address 0x0 segfaults because the OS deliberately leaves the lowest page unmapped, precisely so that the universal mistake of dereferencing a null pointer is caught instantly rather than corrupting real data.
Undefined behavior, alignment, and why -O2 lies
Every dangling-pointer dereference is undefined behavior, and it is worth being exact about what that phrase means, because the casual reading is wrong. It does not mean "the result depends on your platform." It means the C standard places no requirement on the program at all — and crucially, the optimizer is allowed to assume undefined behavior never happens. So when your code dereferences a freed pointer, the compiler may reason "this access is valid (because it must be), therefore the pointer is non-null, therefore I can delete the null check three lines down." The bug is not just a wrong value; it can reshape code far away from the dereference.
This is exactly why a memory bug can vanish at `-O0` and corrupt at `-O2`, and why "but it works on my machine" is no defense. At low optimization the compiler does little reasoning, so the broken code limps along on whatever bytes happen to be there; crank the optimizer up and it acts on assumptions your buggy code violated, and the behavior changes completely. The lesson from guide 2 holds with full force here: do not trust a program that only appears to work. Undefined behavior is a contract you broke, after which the standard — and your optimizer — owe you nothing.
One quieter relative deserves a line: alignment. Each type wants to sit at an address that is a multiple of its size — a 4-byte `uint32_t` at a multiple of 4, an 8-byte pointer at a multiple of 8 — and the compiler arranges honest variables to satisfy this. You can only break it by being clever, such as casting a `char *` to a wider pointer at an arbitrary offset, manufacturing a misaligned address. On some CPUs that merely runs slowly; on others the access faults outright. As ever: respect the type, and alignment takes care of itself.
Making the silent bugs loud
Because the worst dangling-pointer bugs do not crash, the working programmer's real defense is tooling that turns silence into a loud, located report. The single most valuable tool here is the address sanitizer: build with `gcc -fsanitize=address -g main.c`, and it instruments every memory access so that a use-after-free, a returned-local dereference, or an out-of-bounds touch prints the exact line and a full trace the moment it happens — including where the block was allocated and where it was freed. It is the difference between a clueless segfault and a bug report that hands you the answer.
- Build with the sanitizer and debug info: gcc -fsanitize=address -g main.c, then run ./a.out so the instrumentation is active while the bug fires.
- Read the report top-down: it names the kind of error (heap-use-after-free, stack-use-after-return), the offending line, and the allocation and free sites that set the trap.
- Fix the lifetime, not the symptom: shorten a pointer's reach so it never outlives its storage, set freed pointers to NULL, or move the data to the heap when it must outlive the frame that made it.
That last step is the whole rung in one sentence. A pointer is only ever as trustworthy as the lifetime of the storage it names — and keeping those two clocks in sync is the discipline that separates safe systems code from the dangerous kind. This is also the exact problem Rust's ownership and borrow checker were built to catch at compile time, an honest contrast we will reach later in the ladder: not magic, not the end of all bugs, but a different set of tradeoffs that makes the dangling pointer a compile error instead of a 2 a.m. core dump. For now you have what C gives you — your own care, and tools sharp enough to make carelessness loud.