A pointer is a variable that holds a memory address rather than a value directly. Instead of storing 42, it stores “the location in memory where 42 lives.” You follow the pointer to get to the actual value — this is called dereferencing.
Pointers are useful when:
You want to refer to a large value without copying it
You need multiple parts of your program to refer to the same data
You need to allocate data on the heap rather than the stack
[!info] Rust is pass by value
Rust is a pass-by-value language — when you pass a variable to a function, the value is either moved or copied into the function. There is no implicit pass-by-reference.
To avoid moving or copying, you explicitly pass a pointer (a reference) instead:
fn print(s: String) { ... } // takes ownership — s is moved infn print(s: &String) { ... } // borrows — s is a pointer to the original
This is why & appears so often in Rust function signatures. It’s not pass-by-reference in the traditional sense — you’re explicitly constructing and passing a pointer.
Rust has several pointer types beyond plain references. Each solves a specific problem around ownership, heap allocation, or sharing data.
References (recap)
Plain references (&T, &mut T) are the most common pointer type — covered in [[borrowing]]. They are always valid, never null, and enforced by the borrow checker at compile time.
Box<T> — heap allocation
Box<T> puts a value on the heap and gives you an owned pointer to it. The value is dropped when the Box goes out of scope.
let b = Box::new(5);println!("{b}"); // 5 — Box derefs transparently
Use Box when:
You have a large value you want on the heap instead of the stack
You need a type whose size isn’t known at compile time (e.g. recursive types)
// recursive type — without Box this would be infinitely sizedenum List { Cons(i32, Box<List>), Nil,}
Rc<T> — shared ownership
Rc<T> (reference counted) allows multiple owners of the same heap value. It keeps a count of how many owners exist and drops the value when the count reaches zero.
use std::rc::Rc;let a = Rc::new(String::from("hello"));let b = Rc::clone(&a); // increments the count, does not copy the datalet c = Rc::clone(&a);println!("{}", Rc::strong_count(&a)); // 3
Rc is for single-threaded scenarios only. Use Arc for multiple threads.
The values inside Rc are immutable — you can’t get a mutable reference to the inner value.
Arc<T> — shared ownership across threads
Arc<T> (atomic reference counted) works like Rc but is safe to share across threads. The atomic operations make it slightly slower than Rc.
use std::sync::Arc;use std::thread;let value = Arc::new(42);let value_clone = Arc::clone(&value);thread::spawn(move || { println!("{value_clone}");});
RefCell<T> — interior mutability
RefCell<T> lets you mutate a value even when you only have an immutable reference to it. Instead of the borrow checker enforcing rules at compile time, RefCell enforces them at runtime — and panics if you violate them.
use std::cell::RefCell;let data = RefCell::new(vec![1, 2, 3]);data.borrow_mut().push(4); // mutable borrowprintln!("{:?}", data.borrow()); // immutable borrow — [1, 2, 3, 4]
RefCell is often combined with Rc to get shared, mutable data:
Raw pointers (*const T, *mut T) exist for low-level and FFI (foreign function interface) code. Unlike references, they can be null and are not checked by the borrow checker. Using them requires an unsafe block.
let x = 42;let raw = &x as *const i32;unsafe { println!("{}", *raw);}
Avoid raw pointers unless you’re writing unsafe code, interfacing with C libraries, or building low-level abstractions.