Lesson 04 / 15
Null and Dangling Pointers
Why a null pointer is a checkable condition, while a dangling pointer keeps pointing at a freed block and is therefore never run; how both are two faces of the unowned class.
Published Updated
In this lesson
- Shows with an example how to check a null pointer before dereferencing it
- Explains that a dangling pointer points at a freed block and why this condition is never run
- Measures how assigning NULL moves a pointer out of the unowned class by making it checkable
Contents
The previous lesson’s last line assigned NULL to a pointer right after a free call and
left it as “the topic of the next lesson.” This lesson measures why that assignment is a
mandatory habit: a pointer that keeps pointing at a freed block becomes a dangling
pointer, and this condition is one of two observations that fall into the core’s unowned
class — no one is obligated to keep this pointer valid, and no one guarantees pulling it to
NULL either.
Null Pointer: A Checkable Condition
// null_pointer.c — a null pointer becomes safe once checked before dereferencing
#include <stdio.h>
#include <stdlib.h>
static int *maybe_allocate(int succeed) {
if (!succeed) {
return NULL;
}
int *p = malloc(sizeof *p);
if (p != NULL) {
*p = 9;
}
return p;
}
int main(void) {
int *failed = maybe_allocate(0);
printf("%s %d\n", "is the failed call null", (int)(failed == NULL));
int *succeeded = maybe_allocate(1);
if (succeeded != NULL) {
printf("%s %d\n", "value from the successful call", *succeeded);
} else {
printf("%s\n", "successful call unexpectedly null");
}
free(succeeded);
return 0;
}
is the failed call null 1 value from the successful call 9
PO17. The call maybe_allocate(0) returns NULL, and the comparison
failed == NULL gives 1 — this shows a null pointer is an expected and checkable
condition. The call maybe_allocate(1) returns a real block; but this block, too, is
checked with succeeded != NULL before use, because malloc gives no guarantee of
succeeding under every condition. Checking a null pointer before dereferencing it is
this measurement’s only rule: *failed was never written, because failed was already
known to be null.
Dangling Pointer: Continuing to Point at a Freed Block
After a pointer is freed with free, the value it carries does not change — it still
points at the same block, but that block is no longer valid. This condition is called a
dangling pointer. Dereferencing a dangling pointer (reading or writing through *p)
or handing it to free again (a double free) is undefined behavior: the result is most
often a silently wrong value, sometimes a crash, never reliable behavior. That is why this
lesson never dereferences or re-frees any dangling pointer — it only describes why this
is dangerous and runs its defined counterpart.
// defined_counterpart.c — assigning NULL after freeing makes the dangling condition checkable
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof *p);
if (p == NULL) {
printf("%s\n", "allocation failed");
return 1;
}
*p = 21;
printf("%s %d\n", "value before freeing", *p);
free(p);
p = NULL;
printf("%s %d\n", "is it null after freeing", (int)(p == NULL));
if (p != NULL) {
printf("%s %d\n", "branch never reached", *p);
} else {
printf("%s\n", "dereference skipped thanks to the check");
}
return 0;
}
value before freeing 21 is it null after freeing 1 dereference skipped thanks to the check
PO18. p = NULL is assigned right after the free(p) call; no other operation
comes between these two lines. The comparison p == NULL gives 1, and the final if
block never reaches the *p line because p != NULL is false — the line “dereference
skipped thanks to the check” is the proof of this. Had the p = NULL assignment been
skipped, p would have become a dangling pointer still pointing at the freed block, and
the p != NULL check could not have caught it — because a dangling pointer’s value is not
null, only invalid. This is the fundamental distinction between a null pointer and a
dangling pointer: one is a checkable value (null), the other is an uncheckable
condition (invalid but not appearing null).
A second danger of a dangling pointer is handing it to free again — this condition is
called a double free. After the free(p) call, if p still carries the old address, a
second free(p) call tries to free the same block twice; this too is undefined behavior
and usually corrupts the allocator’s own internal state. The example above never writes
this second call, because calling free(NULL) after the p = NULL assignment is
harmless — the standard defines freeing a null pointer as doing nothing. This is a
second benefit of the NULL assignment: it makes not only dereferencing but also a
possible second free call harmless.
// safe_second_call.c — free(NULL) is defined as harmless by the standard
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof *p);
if (p == NULL) {
printf("%s\n", "allocation failed");
return 1;
}
*p = 4;
printf("%s %d\n", "value before freeing", *p);
free(p);
p = NULL;
free(p);
printf("%s\n", "second free ran on null");
return 0;
}
value before freeing 4 second free ran on null
PO19. This code calls free(p) twice, but a p = NULL line sits between the two
calls. The first call frees the real block; the second call operates on NULL, and because
the standard explicitly defines this as “doing nothing,” the program reaches the line
“second free ran on null” without crashing. Had the p = NULL assignment on the line
between them been missing, this same code’s second free(p) call would have tried to free
the same block twice — the double-free error defined above — and that is why it is not
attempted here.
Both members of the unowned class in this lesson trace back to the same root: the
overwritten pointer observation in the first lesson’s core left a block unreachable by
overwriting the single pointer holding its address without freeing it first — a
leak. A dangling pointer is the opposite-direction error: the block has been freed, but
the pointer keeps pointing at it. Both mean “no one guaranteed this condition,” which is
why both fall into the unowned class.
Multiple Copy Pointers: Why a NULL Assignment Is Not Enough
Both examples so far worked through a single pointer. In real code, multiple pointers can
point at the same block — one performs the free call, but the others stay unaware of the
p = NULL line.
// copy_pointer.c — of two pointers pointing at the same block, only one is pulled to NULL
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof *p);
if (p == NULL) {
printf("%s\n", "allocation failed");
return 1;
}
*p = 7;
int *q = p;
printf("%s %d\n", "do p and q show the same value", (int)(*p == *q));
printf("%s %d\n", "do p and q carry the same address", (int)(p == q));
free(p);
p = NULL;
printf("%s %d\n", "is p null after freeing", (int)(p == NULL));
return 0;
}
do p and q show the same value 1 do p and q carry the same address 1 is p null after freeing 1
PO20. After the line int *q = p;, p and q carry the same address — both the
*p == *q and p == q comparisons give 1. When free(p) runs and then p = NULL,
only the p variable itself is affected; q never appears in these two lines, so it
still carries the old address of the block that was freed with free — q is now a
dangling pointer. This code neither dereferences q after this point nor compares it
against NULL, because the value q carries is now invalid and trying to read it (even a
comparison) is undefined behavior. What this is meant to show is not visible while the
code runs — which is exactly the bound itself: p = NULL only changes the condition of the
variable named p, not the condition of q.
Dangling Pointers Across the Ownership Axis: Three Different Sources
Both examples up to this point produced the dangling condition from a single source:
freeing a malloc block with free. But a dangling pointer can arise a different way in
each of the course’s four ownership classes (language, caller, callee, unowned);
none of this code is run here because each leads to undefined behavior, but all three tie
back to the same root this lesson measures.
In the language class, if a function returns the address of a local variable (something
like return &local;), the returned pointer becomes dangling the instant the function
returns — because it is the language itself that manages local‘s lifetime, and that
lifetime is bound to the function’s body. There is no free call here at all; the block
disappears not through a malloc, but through the call frame itself going away. The result
is still the same: the caller is left holding a pointer to an invalid address.
In the caller class, the dangling condition is the form seen in both this lesson’s
examples: if the side that allocated the memory and carries the obligation to call free
forgets the NULL assignment after freeing (or carries a second copy, as in
copy_pointer.c), that pointer stays dangling.
In the callee class, the dangling condition comes from the opposite direction: if a
function frees a block it holds itself (think of the data a core like owner.c owns)
internally with free, but does not pull back a pointer it previously handed to the
caller for that block, the caller’s own copy is now a dangling pointer — the caller has no
way of knowing this, because the callee never reported it in any way. This is why a
function in the callee class must have an explicit contract: documenting when it frees a
block, and that this freeing invalidates the caller’s copies, is the callee side’s
responsibility.
All three arrive at the same observation: a dangling pointer is a question about whether
the address a pointer carries still points at a valid block, and regardless of which
class it comes from, this question is never answered by the language itself — the answer
always falls to the programmer to track. The difference between the three sources is
when the question becomes noticeable: the error in the language class usually
surfaces the instant the function returns, on first use; the error in the caller class
can be delayed by a forgotten NULL assignment, as in this lesson; the error in the
callee class is the latest to be noticed, because nothing changes in the caller’s own
code at all — only the internal behavior of the function it calls has changed.
The Difference from Rust’s Borrow Checker
The question of who watches over freed memory in C sits in a different place from Rust’s
borrow checker, covered in the Ownership and the Memory Model course (M10/K02): in Rust, a
value’s lifetime is determined at compile time, and a reference whose lifetime has ended is
rejected by the compiler; in C, no mechanism checks whether a pointer still points at a
valid block, aside from a habit like p = NULL. In Rust, something resembling the
copy_pointer.c example above would not compile at all — the instant p is lent to
q, the borrow checker would prevent p from being freed while q is still alive. C has
no such check; knowing that q is dangling is left entirely to the kind of tracking a
programmer does, as done here in this lesson.
A Tool Catches the Dangling Pointer, Not the Compiler
Every check used in this lesson (== NULL, != NULL) is hand-written, added by the
programmer themself — the compiler itself does not know whether a pointer is dangling and
never flags it automatically. In real projects, this gap is closed with runtime tools: a
dynamic analyzer tracks every malloc/free pair while a program runs, and reports it if a
block is accessed (dereferenced, double-freed) after being freed. These tools work not at
compile time but at runtime, and only for paths actually triggered in that particular
run — unlike Rust’s compile-time borrow checker, a dangling-pointer error must actually run
at least once before it is caught. This sums up the nature of defense in C: the language
itself gives no guarantee, but discipline (the NULL-assignment habit seen in this lesson)
and tools (runtime analyzers) together make up for that missing guarantee.
The Bounding Measurement
The p = NULL assignment does not eliminate the dangling-pointer problem — it only
makes it visible for that one pointer. The copy_pointer.c example above showed this
concretely: q, despite carrying the same address as p, was never affected by the
p = NULL line at all. This lesson measured the single-pointer and two-pointer cases
separately; more complex sharing patterns — three or more pointers sharing the same block,
or a pointer stored in a struct field — are the topic of the course’s Dynamic Memory topic,
in the Deallocation Discipline lesson. The core question stays the same there: who holds
the authority to free a block, and who informs the other copies that it has been freed.
Summary
- A null pointer (
NULL) is a checkable value; it can be tested with== NULLbefore dereferencing, and this lesson applied that after every allocation call. - A dangling pointer is a pointer that keeps pointing at a freed block; this lesson never dereferenced or re-freed one — it only described it.
- Assigning
NULLto a pointer right after afreecall converts the dangling condition into a checkable null condition. - Null and dangling pointers are two different faces of the core’s unowned class: one
is an unfreed leak, the other is a freed but still-pointed-at block.
p = NULLapplies only to that one pointer; if another copy points at the same block, ascopy_pointer.cshowed, that copy is still dangling. - The dangling condition can arise not only from a
callerforgetting aNULLassignment, but also from a local variable’s address being returned in thelanguageclass, or acalleefreeing a block it holds without informing the caller — all three arrive at the same question (is the address still valid) by a different route. - A double free (calling
freeon the same block twice) is also a consequence of a dangling pointer; the standard definingfree(NULL)as harmless makes thep = NULLhabit protective against this second danger as well —safe_second_call.cshowed this concretely.
Next Step
The four lessons up to this point used pointers’ concrete types, like int * and
double * — the type always said what value was being pointed at. The next lesson,
void and Opaque Pointers, measures conditions where a pointer’s type is deliberately
hidden: the core’s opaque record observation shows how it moves ownership to the
callee class while fully concealing the Record struct’s contents. The dangling
pointer danger seen in this lesson still applies there — because the caller holding an
opaque record’s address has the obligation to free it belonging to the callee, it can
only learn when the record becomes invalid from the callee’s contract; because the record’s
contents are hidden, that contract comes not from the type definition itself, but only from
documentation.
To keep your progress and take notes, Log in
My notes
Log in to take notes.