---
title: 'Null and Dangling Pointers'
source: 'https://academia.sh/en/courses/c-memory/null-and-dangling-pointers'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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

```c
// 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.

```c
// 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.

```c
// 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.

```c
// 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 `== NULL` before
  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 `NULL` to a pointer right after a `free` call **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 = NULL` applies
  only to **that one pointer**; if another copy points at the same block, as
  `copy_pointer.c` showed, that copy is still dangling.
- The dangling condition can arise not only from a `caller` forgetting a `NULL`
  assignment, but also from a local variable's address being returned in the `language`
  class, or a `callee` freeing 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 `free` on the same block twice) is also a consequence of a
  dangling pointer; the standard defining `free(NULL)` as harmless makes the `p = NULL`
  habit protective against this second danger as well — `safe_second_call.c` showed 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.
