Skip to content
All insights

Academy

The Pointer Hierarchy: References, Smart Pointers, and Raw Pointers — Choosing the Right Abstraction for Every Layer

Explore Rust's pointer hierarchy—from guaranteed references and heap-managed smart pointers to unsafe raw pointers—and choose the right abstraction.

6 min read

The Pointer Hierarchy: References, Smart Pointers, and Raw Pointers — Choosing the Right Abstraction for Every Layer
On this page

In C, pointers are everything. They're how you pass data to functions, how you build linked lists, how you access hardware registers, how you index into arrays, and how you create approximately 70% of all security vulnerabilities in systems software. A pointer in C is just a number—an address—with no guarantees about what it points to. It might point to valid data. It might point to freed memory. It might point to the middle of another data structure. It might be null. The language doesn't know and doesn't care. That's your problem.

Rust took the concept of pointers and split it into a hierarchy of types, each with different guarantees and different costs:

References (&T, &mut T): The everyday pointer. Non-null, aligned, valid, non-dangling—guaranteed by the compiler at zero runtime cost. This is what you use 99% of the time.

Smart pointers (Box<T>, Rc<T>, Arc<T>): Owned pointers that manage heap memory automatically. Each provides different ownership semantics (unique, shared single-threaded, shared multi-threaded).

Raw pointers (*const T, *mut T): The C-style pointer. No guarantees. Only usable inside unsafe blocks. Reserved for FFI, low-level optimization, and implementing the abstractions that safe Rust is built on.

This hierarchy is Rust's answer to the pointer problem: give programmers the safe option by default, make the unsafe option available when needed, and make the boundary between safe and unsafe explicit and auditable.

At RantAI, where our systems span from high-level AI orchestration to low-level FFI with C libraries, this pointer hierarchy lets us use the safest abstraction at each layer. This article, drawn from Chapter 10 of our guide "The Rust Programming Language," covers the full spectrum of pointer types in Rust.

References: Safe Pointers That the Compiler Guarantees#

Rust
fn sum(data: &[i32]) -> i32 {
    data.iter().sum()
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let total = sum(&numbers);  // &numbers is a reference (safe pointer)
    println!("Sum: {}, Original: {:?}", total, numbers);
}

References are Rust's primary pointer type. They're pointers with four compile-time guarantees:

  1. Non-null: A reference can never be null. If you need "maybe no value," use Option<&T>.

  2. Aligned: References always point to properly aligned data.

  3. Valid: References always point to initialized data of the correct type.

  4. Non-dangling: References never outlive the data they point to (enforced by lifetimes).

These guarantees cost nothing at runtime. A &T is exactly the same as a *const T at the machine level—a single pointer-sized integer. The guarantees exist entirely in the compiler's analysis.

Mutable References: Exclusive Write Access#

Rust
fn append_sum(data: &mut Vec<i32>) {
    let sum: i32 = data.iter().sum();
    data.push(sum);
}

fn main() {
    let mut numbers = vec![1, 2, 3];
    append_sum(&mut numbers);
    println!("{:?}", numbers);  // [1, 2, 3, 6]
}

&mut T provides exclusive access—while a mutable reference exists, no other references to the data can exist. This exclusivity prevents data races and aliasing bugs.

Smart Pointers: Ownership on the Heap#

Box<T>: Unique Ownership, Heap Allocation#

Rust
fn main() {
    // Box puts data on the heap with unique ownership
    let boxed = Box::new(42);
    println!("Value: {}", *boxed);  // Dereference to access
    
    // Useful for recursive types
    enum List {
        Cons(i32, Box<List>),
        Nil,
    }
    
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}

Box<T> is the simplest smart pointer: it allocates data on the heap, provides unique ownership, and frees the data when the Box goes out of scope. Use it for large data that shouldn't live on the stack, recursive types (which need heap allocation because their size isn't known at compile time), and trait objects (Box<dyn Trait>).

Rc<T>: Shared Ownership, Single-Threaded#

Rust
use std::rc::Rc;

fn main() {
    let shared = Rc::new(String::from("shared data"));
    let clone1 = Rc::clone(&shared);  // Increments reference count
    let clone2 = Rc::clone(&shared);  // Increments again
    
    println!("References: {}", Rc::strong_count(&shared));  // 3
    println!("All see: {} {} {}", shared, clone1, clone2);
}   // All three Rc's dropped, ref count reaches 0, String is freed

Rc<T> enables multiple owners of the same data through reference counting. When the last Rc is dropped, the data is freed. Rc is not thread-safe—use Arc<T> for multi-threaded shared ownership.

Arc<T>: Shared Ownership, Thread-Safe#

Rust
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);
    let mut handles = vec![];
    
    for i in 0..3 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let sum: i32 = data.iter().sum();
            println!("Thread {}: sum = {}", i, sum);
        }));
    }
    
    for h in handles { h.join().unwrap(); }
}

Arc<T> (Atomic Reference Counting) is the thread-safe version of Rc<T>. The reference counting uses atomic operations, making it safe to share across threads.

Raw Pointers: The Unsafe Escape Hatch#

Rust
fn main() {
    let x = 42;
    let raw_const: *const i32 = &x;    // Raw immutable pointer
    let raw_mut: *mut i32 = &x as *const i32 as *mut i32;  // Raw mutable pointer
    
    // Creating raw pointers is safe
    // DEREFERENCING them requires unsafe
    unsafe {
        println!("Value: {}", *raw_const);
    }
}

Raw pointers have no compiler guarantees. They can be null, dangling, misaligned, or aliased. Creating them is safe; using them (dereferencing) requires unsafe. Use raw pointers only for FFI with C libraries, implementing safe abstractions over unsafe operations, and performance-critical code where the borrow checker's restrictions are genuinely too conservative (rare).

The Pointer Hierarchy Decision#

Need Use Safety Read data without owning &T Safe, compile-time verified Modify data without owning &mut T Safe, exclusive access Own data on the heap Box<T> Safe, unique ownership Share ownership (single thread) Rc<T> Safe, reference counted Share ownership (multi thread) Arc<T> Safe, atomic ref counted FFI or unsafe abstractions *const T / *mut T Unsafe, no guarantees

Broader Implications: Safety by Default#

At RantAI, our codebase is 99%+ safe Rust. The unsafe blocks that exist are isolated behind safe interfaces, documented with safety invariants, and reviewed by senior engineers. The pointer hierarchy means we default to the safest option and only reach for raw pointers when we have a specific, justified reason.

Practical Applications & Strategic Takeaways#

For newcomers: Use &T and &mut T for everything. Reach for Box<T> when you need heap allocation. You might never need raw pointers.

For C veterans: Rust's references are what C's pointers would be if the compiler enforced non-null, validity, and exclusive access. Same performance, dramatically more safety.

For library authors: Wrap unsafe pointer operations in safe abstractions. Your users should never see unsafe—they should see a clean, safe API built on top of carefully verified unsafe code.

Our Commitment to Open Knowledge#

RantAI is committed to open education. Pointers and references are covered in Chapter 10 of our guide, "The Rust Programming Language," freely available online.

Explore these concepts further: https://trpl.rantai.dev

Support Our Mission & Get Your Handbook#

How often do you use raw pointers in your Rust code? Never? Occasionally for FFI? Share your pointer usage patterns!

#RustLang #Pointers #References #Box #Arc #RantAI #LearnRust #MemorySafety #SmartPointers #SystemsProgramming

Next article

Shared and Exclusive References: Rust's Pointer Discipline