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 | Heap | |
| Speed | Fast (last in, first out) | Slower (creating + cleaning up) |
| Size | Fixed size, known when building | Changes as program runs |
| Examples | i32, bool, arrays, tuples | String, Vec, Box |
Ownership Rules
.clone() to copy.
- Each value has one owner
- Only one owner at a time
- 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)
.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
&). 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
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.
&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
& = 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.
'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 │ }
<'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.
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
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
- Ownership: one owner per value, dropped when owner goes out of scope
- Move: assigning or passing gives ownership to someone else (except
Copytypes) - Borrowing:
&Tfor reading (many allowed),&mut Tfor writing (only one) - Lifetimes: labels (
'a) make sure refs don't live longer than their data - Drop: automatic cleanup when value is no longer used