Memory: Ownership & Borrowing

This is the thing that makes Rust special. It'll feel weird at first. You'll fight the compiler. That's normal. Once it clicks, you'll wonder how you lived without it.

Stack vs Heap

Stack is your desk — fast, organized, everything has its place, but you need to know how much space you need upfront. Heap is the storage closet — bigger, messier, you can stash things of any size, but finding and cleaning up takes work. Rust's ownership system cleans up automatically: when the owner walks out the door, the closet gets emptied.
StackHeap
SpeedFast (last in, first out)Slower (creating + cleaning up)
SizeFixed size, known when buildingChanges as program runs
Examplesi32, bool, arrays, tuplesString, Vec, Box

Ownership Rules

Think of ownership like a rental agreement on an apartment. You are the tenant (owner). When you move out (go out of scope), you must clean up (drop). You can give the lease to someone else (move). Small things like a coffee mug (i32) are so cheap you just copy them (Copy). Big things like furniture (String) you don't copy by accident — you must say .clone() to copy.
  1. Each value has one owner
  2. Only one owner at a time
  3. When owner goes out of scope, the value is dropped (freed)
 1 │ {
 2 │     let s = String::from("hello");
 3 │     // s owns the heap string
 4 │ } // s goes out of scope → drop() called → memory freed
 5 │
 6 │ // Move (transfer ownership)
 7 │ let s1 = String::from("hello");
 8 │ let s2 = s1;               // s1 MOVED to s2
 9 │ // println!("{s1}");       // ERROR: s1 no longer valid
10 │
11 │ // Clone (deep copy)
12 │ let s1 = String::from("hello");
13 │ let s2 = s1.clone();       // deep copy, both valid
14 │ println!("{s1} {s2}");     // OK
15 │
16 │ // Copy types (just copied, not moved)
17 │ let x = 42;
18 │ let y = x;                 // copy, both valid
19 │ println!("{x} {y}");       // OK (i32 implements Copy)
Legend: 2 s owns the string   4 scope ends, memory freed   8 ownership moves to s2   13 .clone() copies data (both valid)   18 i32 is Copy (cheap to copy)

Copy types: all ints, floats, bool, char, tuples of Copy types. String, Vec, Box are NOT Copy.

Passing to Functions

Passing a value to a function is the same as assigning it — it moves. If you hand someone your laptop, you can't use it anymore. To let someone see your laptop without handing it over, you lend it (borrow with &). They give it back when done.
 1 │ fn take_ownership(s: String) { /* s dropped here */ }
 2 │ fn take_copy(i: i32) { /* i copied, original alive */ }
 3 │ fn give_ownership() -> String { String::from("new") }
 4 │
 5 │ let s = String::from("hello");
 6 │ take_ownership(s);       // s MOVED, can't use s anymore
 7 │ // println!("{s}");      // ERROR
 8 │
 9 │ let n = 42;
10 │ take_copy(n);            // n COPIED, still usable
11 │ println!("{n}");         // OK
Legend: 1 s moved into function, dropped when fn ends   2 i32 is Copy, original stays alive   6 after this, s is gone   10-11 n still usable because i32 is Copy

Borrowing

Pass a reference (&T) to avoid moving. Borrowing lets you use a value without taking ownership.

Quick tip for beginners: If the compiler says "borrow of moved value," you probably need & in your function parameter. 90% of the time, write fn foo(x: &Type) instead of fn foo(x: Type) and you'll avoid move issues.

A library book can be read by many people at the same time (&T) — that's safe because nobody's changing it. But if someone needs to write notes in the book, they need exclusive access (&mut T) — nobody else can read it while they're writing. This is the fundamental Rust trade-off: you can have many readers or one writer, never both.
 1 │ fn read(s: &String) { println!("{s}"); }  // borrow (immutable)
 2 │
 3 │ fn write(s: &mut String) { s.push_str("!"); }  // mutable borrow
 4 │
 5 │ let mut s = String::from("hello");
 6 │
 7 │ // Immutable borrows (&T)
 8 │ let r1 = &s;
 9 │ let r2 = &s;    // multiple immutable borrows OK
10 │ read(&s);
11 │ println!("{r1} {r2}");
12 │
13 │ // Mutable borrows (&mut T)
14 │ let r3 = &mut s;
15 │ write(r3);
16 │ // let r4 = &s;  // ERROR: can't borrow as immutable while mutable exists
Legend: 1 & = borrow (read-only)   3 &mut = borrow with write access   8-9 many readers allowed   14 one writer at a time   16 can't read while writing

Borrowing Rules

  • Many &T (read-only refs) OR one &mut T (read-write ref) — never both at the same time
  • A reference must not live longer than its owner
  • References always point to real data — no dead pointers

Lifetimes

Lifetime labels tell the compiler how long references are usable. They don't change how the program runs.

"You borrowed my book. When does the library close?" That's all a lifetime is — it answers "how long is this reference good for?" The 'a is just a label saying "these two things must stay alive for the same time." Usually the compiler figures it out automatically. You only need to write them when the compiler can't — like when a function returns a reference from one of several inputs.
 1 │ // Explicit lifetime 'a
 2 │ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
 3 │     if x.len() > y.len() { x } else { y }
 4 │ }
 5 │
 6 │ // Struct with a reference
 7 │ struct Excerpt<'a> {
 8 │     part: &'a str,
 9 │ }
10 │
11 │ fn main() {
12 │     let novel = String::from("Call me Ishmael.");
13 │     let first = novel.split('.').next().expect("no .");
14 │     let e = Excerpt { part: first };  // e can't outlive novel
15 │ }
Legend: 2 <'a> declares a lifetime label   2 both params and return share lifetime 'a   7-8 struct holding a ref needs a lifetime   14 Excerpt borrows from novel, so novel must outlive it

When You Can Skip Lifetimes (automatic rules)

  • Each input reference gets its own lifetime
  • If there's one input lifetime, it's assigned to all output references
  • If &self, its lifetime is assigned to all output references

Drop

Drop runs cleanup code when a value goes out of scope. Happens automatically.

Like cleanup code in other languages — but it always runs. When the owner goes out of scope, Rust calls drop() by itself. No manual freeing, no garbage collector, no memory leaks. This is why Rust doesn't need a GC: the compiler adds all the cleanup code when building.
1 │ struct Custom {
2 │     data: String,
3 │ }
4 │
5 │ impl Drop for Custom {
6 │     fn drop(&mut self) {
7 │         println!("Dropping: {}", self.data);
8 │     }
9 │ }
10 │
11 │ {
12 │     let c = Custom { data: "x".to_string() };
13 │ } // "Dropping: x" printed here
14 │
15 │ // Can call drop() by hand with std::mem::drop
16 │ let c = Custom { data: "y".to_string() };
17 │ std::mem::drop(c);  // drop by hand
18 │ // c can't be used after
Legend: 5-8 custom cleanup when value goes out of scope   11-13 scope end triggers drop automatically   17 std::mem::drop drops early

Try It Yourself

Task 1: Fix the Ownership

This code won't compile. Fix it without changing the last line — you can only change the first part.

let s1 = String::from("hello");
let s2 = s1;  // s1 is moved here

println!("{}", s1);  // error: s1 is gone
println!("{}", s2);
Show solution
let s1 = String::from("hello");
let s2 = s1.clone();  // copy instead of move

println!("{}", s1);
println!("{}", s2);

// Or: borrow &s1 instead of moving

Task 2: Sum a Slice

Write a function that takes a slice of i32 and returns the sum. The caller should still own the Vec after calling the function.

fn sum_slice(nums: &[i32]) -> i32 {
    // your code
}

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let total = sum_slice(&v);
    println!("Sum: {total}");   // should work
    println!("v: {v:?}");       // should work too — v not moved
}
Show solution
fn sum_slice(nums: &[i32]) -> i32 {
    let mut sum = 0;
    for n in nums {
        sum += n;
    }
    sum
}

Task 3: Longest String (Lifetimes)

Write a function longest that takes two string slices and returns the longer one. It needs a lifetime annotation.

fn longest(x: &str, y: &str) -> &str {
    // your code
    // hint: x.len() >= y.len()
}
Show solution
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("hello");
    let s2 = "world!!";
    let result = longest(&s1, s2);
    println!("Longest: {result}");
}

Task 4: Why Won't This Compile?

Explain why this code fails, then fix it while keeping both println! calls.

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;  // two mutable borrows?
    println!("{r1} {r2}");
}
Show solution
// Error: can't borrow `s` as mutable more than once.
// Fix: use separate scopes so the first borrow ends before the second starts.

let mut s = String::from("hello");
{
    let r1 = &mut s;
    println!("{r1}");
}
let r2 = &mut s;
println!("{r2}");

// Or: use immutable borrows if you only need to read
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}");

Summary