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:

  1. Non-null — a reference can never be NULL. There is no Option<&T> trick that makes a reference null; if you need an optional reference, the reference itself is always valid when present.
  2. Well-aligned — the address the reference points to is correctly aligned for the type.
  3. Points to a valid, initialized value for the entire lifetime of the reference.
  4. 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 Box fixes 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 Box keeps 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, and Box are built on raw pointers internally. Their public API is safe, but the internals use unsafe and raw pointers to manage memory.
  • Performance-critical code. In rare cases, eliding bounds checks or avoiding the aliasing restrictions of &mut T requires 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 TBox<T>*const T / *mut T
Non-nullguaranteedguaranteednot guaranteed
Well-alignedguaranteedguaranteednot guaranteed
Points to valid, initialized Tguaranteedguaranteednot guaranteed
Exclusive mutable accessfor &mut Tyes (owns value)none
Automatic deallocationno (borrows)yes (owns)no
Can be moved without copying Tyes (copy pointer)yes (move pointer)yes (copy pointer)
Requires unsafe to dereferencenonoyes

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 &mut references manually. Even through raw pointers, if you create two &mut references 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 unsafe when 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::new for data that could live on the stack. A Box is 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 T does nothing. If you obtained the pointer from Box::into_raw, you must later call Box::from_raw to 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.