Skip to main content
Rust advanced Lesson 22 of 30

Memory Management in Rust

Understand Box<T>, Rc<T>, RefCell<T>, Arc<T>, memory layout, and the basics of unsafe Rust.

Box<T> — Heap Allocation

Box<T> allocates a value on the heap and gives you a single-owner pointer to it.

fn main() {
    let b = Box::new(5);
    println!("b = {}", b); // dereferences automatically

    // Explicit dereference
    println!("*b = {}", *b);

    // Useful for large values (avoids stack overflow)
    let big_array = Box::new([0u8; 1_000_000]);
    println!("allocated {} bytes on heap", big_array.len());
} // big_array is freed here

Recursive Data Structures

Without Box, the size of a recursive type would be infinite:

// ERROR: recursive type has infinite size
// enum List { Cons(i32, List), Nil }

// FIX: Box breaks the recursion — pointer has known size
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

impl List {
    fn new() -> Self { List::Nil }

    fn prepend(self, val: i32) -> Self {
        List::Cons(val, Box::new(self))
    }

    fn sum(&self) -> i32 {
        match self {
            List::Cons(val, next) => val + next.sum(),
            List::Nil => 0,
        }
    }
}

fn main() {
    let list = List::new().prepend(3).prepend(2).prepend(1);
    println!("sum: {}", list.sum()); // 6
}

Rc<T> — Reference Counting (Single-Threaded)

Rc<T> allows multiple owners of the same heap value. The value is dropped when the reference count reaches zero.

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("shared"));
    let b = Rc::clone(&a); // increment reference count
    let c = Rc::clone(&a);

    println!("count: {}", Rc::strong_count(&a)); // 3
    println!("{} {} {}", a, b, c);

    drop(b);
    println!("count after drop: {}", Rc::strong_count(&a)); // 2
} // a and c dropped here, count -> 0, string freed

Rc<T> is not thread-safe (!Send). Use Arc<T> for multi-threaded scenarios.

RefCell<T> — Interior Mutability

RefCell<T> allows mutation through a shared reference by deferring borrow checks to runtime:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    // Multiple shared borrows
    {
        let r1 = data.borrow();
        let r2 = data.borrow();
        println!("{:?} {:?}", *r1, *r2);
    } // r1, r2 released

    // Mutable borrow
    data.borrow_mut().push(4);
    println!("{:?}", data.borrow());
}

Rc<RefCell<T>> — Shared Mutable Data

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<RefCell<Node>>>,
}

impl Node {
    fn new(value: i32) -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(Node { value, children: Vec::new() }))
    }

    fn add_child(parent: &Rc<RefCell<Node>>, child: Rc<RefCell<Node>>) {
        parent.borrow_mut().children.push(child);
    }
}

fn main() {
    let root = Node::new(1);
    let child1 = Node::new(2);
    let child2 = Node::new(3);

    Node::add_child(&root, Rc::clone(&child1));
    Node::add_child(&root, child2);

    println!("root children: {}", root.borrow().children.len()); // 2
}

Arc<T> — Atomic Reference Counting (Multi-Threaded)

Arc<T> is Rc<T> with atomic operations — safe to share across threads:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let shared = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..5 {
        let s = Arc::clone(&shared);
        handles.push(thread::spawn(move || {
            *s.lock().unwrap() += 1;
        }));
    }

    for h in handles { h.join().unwrap(); }
    println!("{}", *shared.lock().unwrap()); // 5
}

Cell<T> — Copy-Type Interior Mutability

Cell<T> is a lightweight alternative to RefCell<T> for Copy types:

use std::cell::Cell;

struct Config {
    debug: Cell<bool>,
    level: Cell<u32>,
}

impl Config {
    fn new() -> Self {
        Config { debug: Cell::new(false), level: Cell::new(1) }
    }

    fn enable_debug(&self) { self.debug.set(true); }
    fn set_level(&self, l: u32) { self.level.set(l); }
}

fn main() {
    let cfg = Config::new();
    cfg.enable_debug(); // mutate through &self!
    cfg.set_level(3);
    println!("debug={}, level={}", cfg.debug.get(), cfg.level.get());
}

Memory Layout

Understanding how Rust lays out data in memory:

fn main() {
    use std::mem::{size_of, align_of};

    println!("bool:   size={} align={}", size_of::<bool>(), align_of::<bool>());
    println!("u8:     size={} align={}", size_of::<u8>(), align_of::<u8>());
    println!("i32:    size={} align={}", size_of::<i32>(), align_of::<i32>());
    println!("f64:    size={} align={}", size_of::<f64>(), align_of::<f64>());
    println!("&str:   size={}", size_of::<&str>());   // 16 (ptr + len)
    println!("String: size={}", size_of::<String>()); // 24 (ptr + len + cap)
    println!("Vec<u8>: size={}", size_of::<Vec<u8>>()); // 24
    println!("Box<i32>: size={}", size_of::<Box<i32>>()); // 8 (pointer)
    println!("Option<&i32>: size={}", size_of::<Option<&i32>>()); // 8 (null opt.)
    println!("Option<i32>: size={}", size_of::<Option<i32>>()); // 8
}

Unsafe Rust

unsafe unlocks five additional capabilities. Use sparingly and document invariants:

fn main() {
    // 1. Dereference raw pointers
    let x = 42;
    let r = &x as *const i32; // create raw pointer (safe)

    unsafe {
        println!("{}", *r); // dereference (unsafe)
    }

    // 2. Mutable raw pointer
    let mut y = 10;
    let p = &mut y as *mut i32;
    unsafe {
        *p += 1;
    }
    println!("{}", y); // 11
}
// 3. Call unsafe functions
unsafe fn dangerous(ptr: *const u8, len: usize) -> &'static str {
    let slice = std::slice::from_raw_parts(ptr, len);
    std::str::from_utf8(slice).unwrap()
}

// Safe wrapper
fn safe_from_bytes(bytes: &[u8]) -> &str {
    unsafe { dangerous(bytes.as_ptr(), bytes.len()) }
}

fn main() {
    let bytes = b"hello";
    println!("{}", safe_from_bytes(bytes));
}
// 4. Implementing unsafe traits
unsafe trait Zeroable {}
unsafe impl Zeroable for u8 {}
unsafe impl Zeroable for i32 {}

fn zero_out<T: Zeroable>(val: &mut T) {
    unsafe {
        let size = std::mem::size_of::<T>();
        let ptr = val as *mut T as *mut u8;
        std::ptr::write_bytes(ptr, 0, size);
    }
}

Smart Pointer Summary

TypeOwnershipThread safeInterior mutUse case
Box<T>UniqueYes (T: Send)NoHeap alloc, recursive types, dyn Trait
Rc<T>SharedNoNoSingle-threaded shared ownership
Arc<T>SharedYesNoMulti-threaded shared ownership
Cell<T>UniqueNoYes (Copy)Cheap interior mutation
RefCell<T>UniqueNoYes (any)Runtime borrow checking
Mutex<T>SharedYesYesThread-safe mutation
RwLock<T>SharedYesYesMany readers OR one writer

Frequently Asked Questions

When should I use Box<T>?
Use Box<T> when you need to heap-allocate a value, when you have a recursive data structure (a type that contains itself), or when you need to return a trait object from a function.
What is the difference between Rc<T> and Arc<T>?
Both are reference-counted pointers for shared ownership. Rc<T> is single-threaded (not Send); Arc<T> uses atomic operations and is safe to share across threads.
What does unsafe mean in Rust?
unsafe unlocks five capabilities not available in safe Rust: dereferencing raw pointers, calling unsafe functions, accessing mutable statics, implementing unsafe traits, and accessing union fields. It does not disable the borrow checker.