root.system / 0x09 / indirection

A number
that means somewhere.

A pointer is just an integer, the same kind covered on the binary page. What makes it different is the meaning we give it: this number is the address of something else in memory. Every dynamic data structure, every reference, every callback, every syscall buffer in your program is built from this single idea. So are most of the famous bugs in the history of software.

Beginner// level 01

What's a pointer?

You already saw, on the memory page, that every byte in your program's address space has a number stamped on it. A pointer is a variable whose value is one of those numbers. Read through the pointer (dereference it) and you read the byte at that address. Write through the pointer and you write the byte at that address.

That's the whole mechanism. Every "reference", "handle", "object", and "ID" in every language is, somewhere underneath, this idea.

POINTERVALUE IN MEMORYp = 0x40008 bytesx = 424 bytesat 0x4000

On a 64-bit system, a pointer is always 8 bytes wide, no matter what it points at. A pointer to an i32 is 8 bytes. A pointer to a 1 GB array is 8 bytes. A pointer to another pointer is 8 bytes. The type attached to a pointer is the compiler's way of remembering how to interpret the bytes at the destination; the pointer itself is always just one address.

Declare, take an address, dereference

Rust• • •
fn main() {
    let x: i32 = 42;

    // RAW pointer: a number that happens to be an address.
    // `*const i32` reads "pointer to a constant i32".
    let p: *const i32 = &x;

    println!("x lives at  : {:p}", &x);
    println!("p stores    : {:p}", p);

    // Dereferencing a raw pointer is `unsafe` because Rust can't
    // prove the address is still valid. With a known-good pointer
    // like this one, it's fine; in general, all the C bugs apply.
    let value = unsafe { *p };
    println!("*p          : {}", value);

    // The idiomatic Rust pointer is a *reference*, written `&T`.
    // The compiler tracks how long it's valid (its lifetime) and
    // refuses to compile code that could dereference a dead one.
    let r: &i32 = &x;
    println!("*r          : {}", *r);
}
C• • •
#include <stdio.h>

int main(void) {
    int x = 42;

    // A pointer is a variable whose value is an address.
    // `int *` reads "pointer to an int".
    int *p = &x;

    printf("x lives at  : %p\n", (void*)&x);
    printf("p stores    : %p\n", (void*)p);

    // Dereference. The compiler doesn't check that p is valid.
    printf("*p          : %d\n", *p);

    // Write through the pointer.
    *p = 100;
    printf("x is now    : %d\n", x);
    return 0;
}
// the three operators
&x says "the address of x." *p says "the thing at the address stored in p." p->field (C) and p.field (Rust) are shortcuts for "follow the pointer, then read the field." Once you internalise these three, every pointer-using language reads the same.
Intermediate// level 02

Why pointers exist

If pointers are just numbers that happen to be addresses, why do we go to so much trouble over them? Because they enable five things that nothing else can.

reason 01
Sharing without copying
Pass a 1 GB image to a function by handing it the 8-byte address instead of copying the bytes. Every fast language uses pointers (or references, which are pointers in disguise) for this.
reason 02
Dynamic allocation
When the size of a thing is only known at runtime, the bytes live on the heap (see /variables) and a pointer on the stack tells you where to find them. Vec, String, malloc: all the same shape.
reason 03
Recursive structures
Linked lists, trees, and graphs cannot exist without pointers. A node embedding its successor is impossible; a node pointing at its successor is trivial.
reason 04
Polymorphism
Function pointers and vtables let one call site dispatch to many implementations. Every plug-in system, every virtual method, every callback is a pointer-to-code.

And the fifth reason is the one closest to the hardware: talking to the world. Memory-mapped device registers, DMA buffers, syscall arguments, file mappings (the mmap trick from the OS page), shared memory between processes. Every one of those is a pointer that means something to the kernel, the device, or another process. Pointers are the universal handle.

A linked list, in two languages

Linked lists are the canonical pointer example. Each node owns a value and a pointer to the next node. In C, that pointer is a raw struct Node *; in Rust, it's an Option<Box<Node>>, which is just a nullable owned pointer. The shape is identical. The guarantees are not.

Rust• • •
// Same shape in Rust: each node *owns* the next node, expressed
// as `Option<Box<Node>>`. None marks the end of the list.
struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

fn cons(value: i32, next: Option<Box<Node>>) -> Box<Node> {
    Box::new(Node { value, next })
}

fn main() {
    let head = cons(1, Some(cons(2, Some(cons(3, None)))));

    let mut cur = Some(&*head);
    while let Some(node) = cur {
        print!("{} ", node.value);
        cur = node.next.as_deref();
    }
    println!();

    // No free() needed. `head` goes out of scope here; Drop
    // walks the chain and releases every allocation in order.
}
C• • •
#include <stdio.h>
#include <stdlib.h>

// A self-referential struct: each node holds a pointer to the next
// node, or NULL at the end. This is impossible without pointers.
typedef struct Node {
    int value;
    struct Node *next;
} Node;

Node *cons(int v, Node *next) {
    Node *n = malloc(sizeof *n);
    n->value = v;
    n->next  = next;
    return n;
}

int main(void) {
    // Build the list  1 -> 2 -> 3 -> NULL.
    Node *head = cons(1, cons(2, cons(3, NULL)));

    // Walk it.
    for (Node *cur = head; cur; cur = cur->next)
        printf("%d ", cur->value);
    putchar('\n');

    // Free it. Forget this step and the memory leaks.
    while (head) {
        Node *next = head->next;
        free(head);
        head = next;
    }
    return 0;
}
// pointer-chasing has a price
Every dereference is a load instruction. The CPU page covered why this matters: if the next node lives in a different cache line, the load stalls for tens to hundreds of cycles. A Vec<T> beats a linked list on almost every modern workload, because contiguous memory is what caches were built for.
Advanced// level 03

Why pointers are dangerous (and how Rust changes the game)

A pointer is an unchecked promise. The compiler trusts that the address it stores is valid; the type system trusts that the bytes there match the declared type; the programmer trusts the address won't be reused or released while still in use. Each of those trusts is a bug waiting to happen.

The five classic pointer bugs

bugwhat happenstrigger
Null dereferenceRead through a pointer that's NULL or nullptr. Usually a segfault.Forgetting to check malloc's return, or following a missing parent in a tree.
Use-after-freeRead or write through a pointer to memory that's already been released.Freeing one alias while another still points at the same allocation.
Double freeCalling free twice on the same address. Corrupts the allocator's bookkeeping; later allocations alias or crash.Two pointers to the same block, both calling free.
Wild pointerDereferencing an uninitialised pointer. Reads from a random address.Declaring int *p; in C and using it without first assigning a real address.
Out-of-boundsPointer arithmetic that walks past the end of an allocation.p + n where n exceeds the buffer length. The basis of most buffer-overflow exploits.

Every one of those is undefined behaviour in C. The compiler is allowed to assume they never happen, so the resulting program can do anything when they do. Decades of CVEs are precisely these five bugs.

The same mistake, two languages

Rust• • •
// The same logical mistake. Rust refuses to compile it.
fn main() {
    let a = Box::new(42);
    let b = &a;            // borrow: b lives only as long as a does.

    println!("{}", *a);

    drop(a);               // explicitly release.

    // println!("{}", **b);
    //                 ^^ error[E0382]: borrow of moved value: `a`
    //
    // The borrow checker tracked the lifetime of `b` and saw it
    // outlived `a`. Compilation stops; no binary is produced.
    //
    // The entire class of "use after free" is eliminated, not by a
    // runtime check, but by refusing to build programs that could
    // express it.
}
C• • •
#include <stdio.h>
#include <stdlib.h>

int *make(int v) {
    int *p = malloc(sizeof *p);
    *p = v;
    return p;
}

int main(void) {
    int *a = make(42);
    int *b = a;          // both pointers alias the SAME allocation.

    printf("%d\n", *a);  // 42, fine.
    free(a);             // the allocator reclaims those 4 bytes.

    // b is now a *dangling pointer*. The compiler said nothing.
    // What this prints depends on what the allocator wrote there
    // next: maybe 42, maybe garbage, maybe a segfault, maybe an
    // attacker-controlled value. All four are valid outcomes of
    // undefined behaviour.
    printf("%d\n", *b);
    return 0;
}

How Rust eliminates four-and-a-half of the five

rule 01
Ownership
Every allocation has a single owner. When the owner goes out of scope, the memory is freed exactly once. Double-free is impossible.
rule 02
Borrowing
A reference (&T or &mut T) must not outlive the owner it points at. The compiler tracks lifetimes and refuses to compile code that could leave a reference dangling.
rule 03
No null references
Safe references are never null. Optional pointers are written Option<&T> or Option<Box<T>>; you can't read them without first checking. Null dereference is impossible.
rule 04
Bounds-checked slices
Indexing into a slice is checked at runtime; out-of-bounds panics rather than corrupts. Pointer arithmetic on raw pointers is allowed only inside `unsafe`.

The "half" Rust doesn't eliminate is memory leaks. You can still leak by holding a reference forever (an Rc cycle, a long-lived Box::leak). Leaks are safe in Rust's safety model; they're bugs but not unsound bugs.

The escape hatch: unsafe and raw pointers

Sometimes Rust's rules are too restrictive. Talking to C code, writing a custom allocator, implementing a lock-free data structure, or reading memory-mapped hardware: all of these need raw pointers. Rust gives them to you. *const T and *mut T behave like C pointers. Dereferencing one requires an unsafe block, which is the language's way of saying "I, the programmer, promise this is sound; the compiler can no longer help."

The standard library is full of unsafe internally: Vec, String, HashMap, every reference-counted type. The point isn't that unsafe is forbidden; it's that most code can be written without it, and the parts that can't are explicitly marked so a reviewer can audit them.

Pointers connect every layer of this site

layerwhere the pointer is
Number systemsAddresses are integers, almost always printed in hex.
BinaryA 64-bit pointer is 8 bytes, little-endian on x86 and ARM.
ASCIIchar * is the most common pointer in C; &str is its bounds-checked Rust cousin.
Logic gatesLoad and store instructions take an address. That address is gated onto the address bus.
CPUThe program counter is a pointer (to the next instruction). So is the stack pointer.
MemoryPointers are how you navigate memory. Without them there is no stack, no heap, no indirection.
Operating systemUser pointers go through the MMU; the kernel resolves them to physical pages. Every syscall buffer is a pointer the kernel validates and copies through.
VariablesDynamic data (Vec, String, Box) is a small fixed-size header containing a pointer to a heap-allocated body.
// the punchline
A pointer is the smallest unit of indirection in computing. Almost every interesting thing software does (data structures, polymorphism, dynamic memory, IPC, drivers, garbage collection, virtual memory itself) is some pattern of pointers on top of pointers. Understanding what they really are, where they live, and what makes them dangerous is the closest thing this site has to a single load-bearing skill.

Where to dig in next

Pointers go deep. A few worthwhile rabbit holes:

  • Smart pointers in Rust (Box, Rc, Arc, RefCell, Cell) and C++ (unique_ptr, shared_ptr, weak_ptr). Each one encodes a different ownership policy in the type system.
  • Pointer tagging: stealing the low bits of an aligned pointer to store extra data. The JVM, V8, and lots of GCs do this. Three free bits per word.
  • Address Sanitizer, Valgrind, Miri: tools that instrument C, C, and Rust respectively to catch use-after-free, leaks, and other pointer crimes at runtime.
  • The CHERI architecture: a CPU with hardware-enforced capabilities, where pointers carry bounds and permissions directly in their bit representation.

Every one of those is a different angle on the same fundamental thing: a number that means somewhere.

next up / 0x0A
When does each piece happen? Compile time vs runtime.
compile vs runtime