title: Pointers

What is a pointer?

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 in
fn 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 sized
enum 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 data
let 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 borrow
println!("{:?}", data.borrow()); // immutable borrow — [1, 2, 3, 4]

RefCell is often combined with Rc to get shared, mutable data:

use std::rc::Rc;
use std::cell::RefCell;
 
let shared = Rc::new(RefCell::new(0));
let clone = Rc::clone(&shared);
 
*shared.borrow_mut() += 1;
println!("{}", clone.borrow());  // 1

Raw pointers

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.

Summary

TypeOwnershipThread safeMutableUse when
&Tborrowedyes (read-only)nomost cases
&mut Tborrowednoyesneed to mutate, single borrower
Box<T>ownedyesyesheap allocation, recursive types
Rc<T>sharednonomultiple owners, single thread
Arc<T>sharedyesnomultiple owners, multiple threads
RefCell<T>ownednoyes (runtime checked)interior mutability
*const T / *mut Tuntrackeduntrackedvariesunsafe / FFI only