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.
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.
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
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);
}#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;
}&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.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.
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.
// 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.
}#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;
}Vec<T> beats a linked list on almost every modern workload, because contiguous memory is what caches were built for.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
| bug | what happens | trigger |
|---|---|---|
| Null dereference | Read 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-free | Read or write through a pointer to memory that's already been released. | Freeing one alias while another still points at the same allocation. |
| Double free | Calling 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 pointer | Dereferencing an uninitialised pointer. Reads from a random address. | Declaring int *p; in C and using it without first assigning a real address. |
| Out-of-bounds | Pointer 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
// 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.
}#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
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
| layer | where the pointer is |
|---|---|
| Number systems | Addresses are integers, almost always printed in hex. |
| Binary | A 64-bit pointer is 8 bytes, little-endian on x86 and ARM. |
| ASCII | char * is the most common pointer in C; &str is its bounds-checked Rust cousin. |
| Logic gates | Load and store instructions take an address. That address is gated onto the address bus. |
| CPU | The program counter is a pointer (to the next instruction). So is the stack pointer. |
| Memory | Pointers are how you navigate memory. Without them there is no stack, no heap, no indirection. |
| Operating system | User pointers go through the MMU; the kernel resolves them to physical pages. Every syscall buffer is a pointer the kernel validates and copies through. |
| Variables | Dynamic data (Vec, String, Box) is a small fixed-size header containing a pointer to a heap-allocated body. |
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.