Pointers & Smart Pointers
Rust gives you full control over memory through references and pointers with extra features.
For beginners: you'll use &T and &mut T 90% of the time. Ignore Cell, RefCell, Cow, and raw pointers until you actually need them. Box comes up with recursive types. Rc/Arc come up with shared ownership. Don't memorize everything — just know where to look.
Quick Reference
| Type | Syntax | Safe? | Use Case |
| Shared Ref | &T |
S | Read-only, many readers |
| Mutable Ref | &mut T |
S | Write access, only one at a time |
| Box | Box<T> |
S | Single owner, heap storage |
| Rc | Rc<T> |
S | Multiple owners, single thread |
| Arc | Arc<T> |
S | Multiple owners, many threads |
| Cell | Cell<T> |
S | Change copyable values through read-only ref |
| RefCell | RefCell<T> |
S | Check borrowing at runtime, single thread |
| Cow | Cow<'a, T> |
S | Starts borrowed, copies if changed |
| Raw Ptr | *const T / *mut T |
Unsafe | Talking to C code, manual memory |
References (&T, &mut T)
Borrow a value without owning it. The compiler checks that it's safe when you build.
1 │ fn len(s: &String) -> usize { s.len() } // borrow, don't take
2 │
3 │ let s = String::from("hello");
4 │ let r = &s; // read-only reference
5 │ len(r);
6 │
7 │ let mut s = String::from("hello");
8 │ let r = &mut s; // read-write reference (only one)
9 │ r.push_str(" world");
&String = borrow, don't take ownership
4 &s = create a read-only reference
8 &mut s = mutable reference (exclusive)
Box<T> — Heap Storage
Store data on the heap (storage closet). Use when the size isn't known when you build, or to move big data without copying it.
Box says "put this on the heap, keep a pointer on the stack." Use it when the size isn't known when building (like types that contain themselves) or when moving big data cheaply. The Box itself is small (just a pointer) and follows normal ownership rules.
1 │ let b = Box::new(5); // i32 on heap
2 │ println!("{}", b); // Rust follows the pointer for you
3 │
4 │ // Recursive type (must use Box)
5 │ enum List {
6 │ Node(i32, Box),
7 │ Nil,
8 │ }
Box::new() allocates on heap
6 Box<List> makes recursive type possible
Rc<T> — Shared Ownership
Multiple owners allowed. Single-thread only. Simple counter (fast).
Rc counts how many people are reading. When the last person finishes, the book gets recycled. This only works in one room (single-thread) because the count is a whiteboard tally, not a lockbox.
1 │ use std::rc::Rc;
2 │
3 │ let a = Rc::new(String::from("hello"));
4 │ let b = Rc::clone(&a); // increments ref count
5 │ let c = Rc::clone(&a); // 3 owners now
6 │ println!("{}", Rc::strong_count(&a)); // 3
7 │
8 │ // Rc::clone is cheap — just increments counter, no deep copy
Rc::new creates shared data
4-5 Rc::clone adds an owner (cheap!)
6 how many owners are alive
8 no deep copy, just a counter bump
Drops when count reaches 0. Use Rc::downgrade for Weak<T> (weak ref, no ownership).
Arc<T> — Thread-safe Shared Ownership
Same as Rc but works across threads. Uses a thread-safe counter (slower but safe).
Rc, but safe across threads. For data that can change, pair with Mutex: Arc<Mutex<T>> — the mutex makes sure only one person writes at a time.
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for _ in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("{:?}", data);
}));
}
// Combine with Mutex for shared data that can change:
// Arc<Mutex<T>>
Cell<T> & RefCell<T> — Changing Through Read-only Refs
Change data through a read-only reference. Skips normal borrowing rules, but there's a cost.
Cell is for small values that can be copied — it swaps values in and out like a post-it note. RefCell is for bigger values — it checks the "one writer or many readers" rule when the program runs instead of when it builds. It can crash if you break the rules. Use these only when the borrow checker won't let you do what you need.
Cell<T>
For Copy types. No borrowing — values are moved in/out.
use std::cell::Cell;
let c = Cell::new(42);
c.set(100);
println!("{}", c.get()); // 100 (copy out)
RefCell<T>
For non-Copy types. Checks borrow rules at runtime (panics on violation).
1 │ use std::cell::RefCell;
2 │
3 │ let v = RefCell::new(vec![1, 2, 3]);
4 │ v.borrow_mut().push(4);
5 │ println!("{:?}", v.borrow()); // [1, 2, 3, 4]
6 │
7 │ // Runtime check: borrow() and borrow_mut() can't coexist
8 │ // Will panic! if rules violated
borrow_mut() gives write access (checked at run time)
5 borrow() gives read access
7-8 breaks rules? program panics
Common pattern: Rc<RefCell<T>> for shared data that can change (single thread only).
Cow<'a, T> — Copy Only When Needed
Starts as borrowed (&T), copies to owned (T) only if you change it.
Cow starts as a borrowed reference (no extra memory). Only if something tries to change it does it copy the data into an owned version. This is useful when most of the time you just read data, but sometimes you need to change it.
use std::borrow::Cow;
fn process(input: &str) -> Cow<str> {
if input.contains(' ') {
// Needs modification → owned
input.replace(' ', "_").into()
} else {
// No changes needed → borrowed
Cow::Borrowed(input)
}
}
let s = process("hello world"); // allocates
let s = process("hello"); // no allocation
Raw Pointers (*const T, *mut T)
C-like pointers. No safety promises. Can only be followed inside unsafe block.
unsafe block is a promise you must check yourself.
let mut x = 10;
let r1 = &x as *const i32; // raw const pointer
let r2 = &mut x as *mut i32; // raw mut pointer
unsafe {
println!("{}", *r1); // follow the pointer in unsafe block
*r2 = 20;
}
Raw pointers can:
- Be null (null pointer)
- Dangle (point to freed memory)
- Ignore sharing rules
- Be sent between threads safely
Use only for talking to C code or when building low-level wrappers.
Combined Patterns
1 │ // Shared data that can change (single-thread)
2 │ use std::{rc::Rc, cell::RefCell};
3 │ let shared = Rc::new(RefCell::new(42));
4 │ {
5 │ let mut val = shared.borrow_mut();
6 │ *val += 1;
7 │ }
8 │
9 │ // Shared data that can change (multi-thread)
10 │ use std::sync::{Arc, Mutex};
11 │ let shared = Arc::new(Mutex::new(42));
12 │ {
13 │ let mut val = shared.lock().unwrap();
14 │ *val += 1;
15 │ }
Rc + RefCell = shared + mutable (1 thread)
11 Arc + Mutex = shared + mutable (many threads)
5-6 borrow_mut() for write inside a scope
13 lock() for thread-safe write
Try It Yourself
Task 1: Linked List with Box
Define a recursive List enum where each node holds an i32 and optionally points to the next node. Use Box — without it the compiler can't know the size.
enum List {
Node(i32, Box),
Nil,
}
// Create: 1 -> 2 -> 3 -> Nil
Show solution
use List::*;
let list = Node(1, Box::new(Node(2, Box::new(Node(3, Box::new(Nil))))));
// You could also add a method to print the list
impl List {
fn print(&self) {
match self {
Node(val, next) => {
print!("{val} -> ");
next.print();
}
Nil => println!("Nil"),
}
}
}
list.print(); // 1 -> 2 -> 3 -> Nil
Task 2: Shared Ownership with Rc
Create a Graph where multiple nodes can share the same child. Use Rc so the child is not dropped while any parent references it.
use std::rc::Rc;
struct Node {
value: i32,
children: Vec>,
}
// Create: root -> child_a, root -> child_b
// child_a -> shared_child
// child_b -> shared_child (same node!)
Show solution
let shared = Rc::new(Node {
value: 3,
children: vec![],
});
let child_a = Node {
value: 1,
children: vec![Rc::clone(&shared)],
};
let child_b = Node {
value: 2,
children: vec![Rc::clone(&shared)],
};
let root = Node {
value: 0,
children: vec![Rc::new(child_a), Rc::new(child_b)],
};
println!("shared ref count: {}", Rc::strong_count(&shared)); // 2
Task 3: Interior Mutability
Use RefCell to create a "mock" object that counts how many times a method is called, even through an immutable reference.
use std::cell::RefCell;
struct Logger {
log_count: RefCell,
}
impl Logger {
fn new() -> Self {
Logger { log_count: RefCell::new(0) }
}
fn log(&self, msg: &str) {
// increment count, print msg
}
fn count(&self) -> u32 {
// return current count
}
}
Show solution
impl Logger {
fn log(&self, msg: &str) {
*self.log_count.borrow_mut() += 1;
println!("[{:?}] {msg}", self.log_count.borrow());
}
fn count(&self) -> u32 {
*self.log_count.borrow()
}
}
fn main() {
let logger = Logger::new();
logger.log("hello");
logger.log("world");
println!("Total: {}", logger.count()); // 2
}
Task 4: Thread-Safe Counter
Spawn 10 threads, each increments a shared counter 100 times. Use Arc<Mutex<i32>> for thread-safe sharing.
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
// increment c 100 times
}));
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // should be 1000
Show solution
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..100 {
let mut val = c.lock().unwrap();
*val += 1;
}
}));
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 1000
When to Use What
- Just reading:
&T - Need to write once (exclusive):
&mut T - Single owner, heap data:
Box<T> - Multiple owners, one thread:
Rc<T> - Multiple owners, many threads:
Arc<T> - Change through
&(copyable types):Cell<T> - Change through
&(non-copyable):RefCell<T> - Shared + changeable (single-thread):
Rc<RefCell<T>> - Shared + changeable (multi-thread):
Arc<Mutex<T>> - Borrowed-or-owned:
Cow<'a, T> - FFI / low-level:
*const T/*mut T