Pointer Types in Rust
A comprehensive guide to Rust's pointer types - references, boxes, and raw pointers - covering usage, guarantees, and common pitfalls
Pointer types let you refer to data stored somewhere else in memory without copying it. Rust provides three families of pointers, each with a different set of compile-time guarantees and runtime costs. References (&T and &mut T) are the safe, everyday workhorses. Box<T> is the simplest owning pointer, putting data on the heap while keeping ownership clear. Raw pointers (*const T and *mut T) are an escape hatch for when you need to operate outside the safety rules. This document walks through how each one works, why it exists, and what can go wrong if you use it incorrectly.
References
A reference is a pointer that the compiler guarantees is valid for as long as the reference exists. It never owns the data it points to — it borrows it.
Shared references (&T)
A shared reference allows many parts of your code to read the same data at the same time. The compiler ensures nobody can modify the data while any shared reference is alive.
fn print_length(s: &String) {
// s is a shared reference. We can read through it, but not mutate.
println!("Length is {}", s.len());
}
fn main() {
let name = String::from("Rust");
print_length(&name); // borrow name immutably
println!("Still own name: {name}");
}
When print_length runs, name is borrowed. The compiler knows that name cannot change until print_length returns, so the final println! is safe. This is the most common pointer you will write in Rust.
Default borrowing:
When you see &variable in function arguments or let bindings, you are creating a shared reference. This is the idiomatic way to let multiple parts of a program inspect data without transferring ownership.
Mutable references (&mut T)
A mutable reference gives exclusive write access to one piece of data. While a mutable reference exists, no other reference — shared or mutable — can reach the same value.
fn append_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let mut message = String::from("Hello");
append_exclamation(&mut message); // borrow message mutably
println!("{message}"); // "Hello!"
}
The key guarantee is aliasing XOR mutability: you can have many readers or exactly one writer, never both. The compiler enforces this at compile time, preventing data races and whole classes of logic bugs that plague languages like C++.
The mental model for a beginner: think of a reference as a temporary ticket that grants read or read-write access. The ticket has an expiry date (the lifetime), and the compiler makes sure you never hold two conflicting tickets at the same time.
Multiple mutable references are rejected:
Trying to create two &mut references to the same data in the same scope will cause a compiler error. This is not a runtime check — the program simply won't build. That compile-time refusal is one of Rust's strongest safety features.
What the compiler guarantees
Every reference in Rust, shared or mutable, satisfies four hard invariants:
- Non-null — a reference can never be
NULL. There is noOption<&T>trick that makes a reference null; if you need an optional reference, the reference itself is always valid when present. - Well-aligned — the address the reference points to is correctly aligned for the type.
- Points to a valid, initialized value for the entire lifetime of the reference.
- No mutable aliasing — for
&mut T, no other reachable reference can point to the same data anywhere in the program.
These invariants mean the compiler can perform optimizations (such as caching reads) that would be impossible in C or C++ without restrict qualifiers.
A common beginner mistake is returning a reference to a value that goes out of scope. The compiler catches this because the lifetime would be too short:
fn dangling() -> &String {
let s = String::from("temporary");
&s // error[E0515]: cannot return reference to local variable `s`
}
The reference would outlive the String, violating the “points to a valid value” guarantee. The compiler rejects it outright.
Boxes (Box<T>)
A Box<T> is a pointer that owns the value on the heap. When the box goes out of scope, it automatically deallocates the memory and drops the contained value.
fn main() {
let b = Box::new(42); // allocate i32 on the heap
println!("Value: {b}"); // prints 42 — Box dereferences automatically
} // b is dropped here; heap memory freed
Box<T> is the simplest form of smart pointer in Rust. It implements the Deref trait, so you can use a Box<T> almost everywhere you would use a &T. The compiler will insert the dereference for method calls and field access.
Why boxes exist
Three problems are awkward or impossible to solve with references and stack allocation alone:
- Recursive types. A type that contains itself directly would have infinite size. A
Boxfixes that because the pointer has a known, fixed size. - Large data structures. Moving a huge struct on the stack can be expensive. Storing it in a
Boxkeeps the stack footprint small. - Ownership transfer without copying. You can move a
Box<T>to transfer ownership of the heap allocation cheaply, without deep-copying the data.
A classic recursive type that requires Box:
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
}
Without Box, the compiler would report that List has infinite size because it would try to embed a List inside itself. Box adds one level of indirection and a fixed pointer size.
Box is not Copy:
If you try to use a Box<T> after moving it, the compiler will stop you. A Box owns its heap allocation; copying the pointer would cause a double-free when both go out of scope. Rust prevents this by making Box<T> non-Copy.
How drop works
When a Box<T> variable goes out of scope, the compiler runs its Drop implementation. First, the inner value’s destructor runs (if T has one). Then the heap allocation is freed. This is fully automatic — no free() calls needed.
This makes Box a building block for other owning pointer types. Internally, standard library types like Vec<T> and String use a raw pointer together with Box-style allocation to manage their heap buffers.
Raw Pointers
Raw pointers, *const T and *mut T, are Rust’s direct analogue to C pointers. They carry none of the guarantees that references do. A raw pointer may be null, misaligned, dangling, or point to uninitialized memory.
Creating raw pointers
You can get a raw pointer by coercing a reference, consuming a Box, or using the raw borrow operators &raw const and &raw mut.
fn main() {
let x = 42;
let r1: *const i32 = &x; // reference coercion
let r2: *const i32 = &raw const x; // raw borrow (for packed/unaligned fields)
let mut y = 99;
let r3: *mut i32 = &mut y;
let heap_val = Box::new(10);
let r4: *const i32 = &*heap_val; // deref the Box, then take address
}
Raw pointers do not enforce any ownership rules. The compiler will not stop you from creating multiple mutable raw pointers pointing to the same location, or from keeping a raw pointer after the original data is freed. That is why they live inside unsafe blocks when you dereference them.
Dereferencing requires unsafe
To read or write through a raw pointer, you must write unsafe:
fn main() {
let mut value = 5;
let ptr: *mut i32 = &mut value;
unsafe {
*ptr += 1; // write through raw pointer
println!("{}", *ptr); // read through raw pointer → 6
}
}
Rust makes the safety contract explicit: you, the programmer, are vouching that the pointer is valid, aligned, and not aliased in a way that would cause undefined behaviour. The compiler trusts you inside unsafe.
Undefined behaviour is a compile-time promise:
Dereferencing a null, misaligned, or dangling raw pointer immediately invokes undefined behaviour. There is no runtime check. The program may appear to work, crash, or corrupt memory silently — all three have been observed in real codebases.
The raw borrow operators
Rust 1.82 stabilised &raw const and &raw mut, which create raw pointers without first creating a reference. This is important when dealing with packed structs or uninitialized memory where creating a reference would be UB.
#[repr(packed)]
struct Misaligned {
a: u8,
b: u32,
}
fn main() {
let s = Misaligned { a: 0, b: 1 };
// &s.b would be UB because the field is unaligned for &u32
let p: *const u32 = &raw const s.b; // safe — no reference created
unsafe {
println!("b = {}", *p);
}
}
Null pointers and the null pointer optimisation
Raw pointers can be null. You can create them with std::ptr::null() or std::ptr::null_mut(), and check with .is_null(). The standard library type NonNull<T> wraps a raw pointer that is guaranteed non-null, enabling the same null-pointer optimisation that references enjoy in Option<&T>.
Prefer NonNull in safe abstractions:
When building a safe wrapper around unsafe code, use NonNull<T> rather than a bare *mut T for pointers that should never be null. This restores the non-null guarantee and lets Option<NonNull<T>> be the same size as a raw pointer.
When raw pointers are the right tool
Raw pointers exist for three main scenarios:
- FFI (Foreign Function Interface). C libraries hand out and expect raw pointers. Rust raw pointers map cleanly to
T*in C. - Building safe abstractions. Data structures like
Vec,HashMap, andBoxare built on raw pointers internally. Their public API is safe, but the internals useunsafeand raw pointers to manage memory. - Performance-critical code. In rare cases, eliding bounds checks or avoiding the aliasing restrictions of
&mut Trequires raw pointers. These cases are rare and should be wrapped in a safe interface with thorough documentation and tests.
For most application code, you will never need to write a raw pointer dereference.
Comparing the Three Pointer Families
| Guarantee | &T / &mut T | Box<T> | *const T / *mut T |
|---|---|---|---|
| Non-null | guaranteed | guaranteed | not guaranteed |
| Well-aligned | guaranteed | guaranteed | not guaranteed |
| Points to valid, initialized T | guaranteed | guaranteed | not guaranteed |
| Exclusive mutable access | for &mut T | yes (owns value) | none |
| Automatic deallocation | no (borrows) | yes (owns) | no |
| Can be moved without copying T | yes (copy pointer) | yes (move pointer) | yes (copy pointer) |
Requires unsafe to dereference | no | no | yes |
Choose a reference when you want to temporarily inspect or modify data someone else owns. Choose a Box when you need to own heap-allocated data and want Rust to manage the memory. Reach for a raw pointer only when the other two cannot do the job — typically when crossing the FFI boundary or building a new safe container type.
Common Mistakes
- Returning a reference to a local variable. The compiler catches most of these, but subtle cases involving loops or conditional scopes can still confuse new Rust programmers.
- Creating two
&mutreferences manually. Even through raw pointers, if you create two&mutreferences to the same data and use them both, you have UB. The aliasing rules apply as soon as a reference is formed, not only when it is used. - Forgetting
unsafewhen dereferencing a raw pointer. The compiler will stop you, but the fix should not be “just add unsafe” — it should be “verify the pointer is valid and aligned first.” - Using
Box::newfor data that could live on the stack. ABoxis an allocation; prefer a local variable unless you need the ownership transfer, recursive type, or size indirection. - Assuming a raw pointer cleans up its pointee. Dropping a
*mut Tdoes nothing. If you obtained the pointer fromBox::into_raw, you must later callBox::from_rawto free the memory. If you allocated with the global allocator, you must deallocate manually.
Raw pointers and drop:
The compiler will never insert a drop call for data behind a raw pointer. If you allocate memory behind a raw pointer and lose the pointer without deallocating, you have a memory leak — or worse, a use-after-free if you reconstruct a Box incorrectly.
Summary
Rust’s pointer types are designed so that the common case — borrowing data — is safe and zero-cost. References enforce the rules at compile time, making whole categories of bugs impossible. Box<T> extends the ownership model to the heap with the same safety guarantees. Raw pointers strip away every guarantee, trading safety for maximum flexibility when you need to talk to C or build new safe abstractions.
The critical insight is that raw pointers are not an “unchecked” version of references; they are a completely different tool. Use references when you can, boxes when you must own heap data, and raw pointers only when the other two physically cannot express the operation you need.