Stack vs heap, the three ownership rules, moves, Clone vs Copy, references, the one-writer-or-many-readers rule, slices, and the borrow errors explained.
Explain what lives on the stack and what lives on the heap, and why that matters for String but not for i32
State the three ownership rules and predict exactly where a value is moved and where it is dropped
Choose between moving, cloning, copying and borrowing when passing values to functions
Apply the rule "one mutable reference OR any number of shared references" and fix E0499 and E0502
Use &str and &[T] slices, and read E0382, E0596 and E0106 without panic
01
Stack vs heap
✓
Ownership is the feature that makes Rust Rust: it is how the language frees memory without a garbage collector and without you calling free. To see why it exists, you need a picture of where values live.
Stack
Fixed-size values known at compile time: i32, f64, bool, char, tuples and arrays of those
Pushed when a function starts, popped when it returns — extremely fast
Each value has exactly one place; copying it is cheap
Heap
Data whose size is only known at runtime or that can grow: the text of a String, the elements of a Vec
Allocated on request, and must be freed exactly once
Reached through a pointer that lives on the stack
A String is really three stack values — a pointer to heap memory, a length and a capacity — plus the bytes on the heap. Somebody has to free those bytes exactly once. Free them too early and you get a use-after-free; twice and you get a double free; never and you leak. Ownership is the rule that says who frees them and when.
rustmain.rs
123456789101112
use std::mem::size_of;fnmain(){let n:i32=42;// all on the stackletmut s =String::with_capacity(16);// pointer+len+cap on the stack, buffer on the heap
s.push_str("hello");println!("i32 on the stack: {} bytes", size_of::<i32>());println!("String handle on the stack: {} bytes", size_of::<String>());println!("s: len {} capacity {}", s.len(), s.capacity());println!("{n} {s}");}
Outputcompiled & run with real Rust
i32 on the stack: 4 bytes
String handle on the stack: 24 bytes
s: len 5 capacity 16
42 hello
On a 64-bit machine a String handle is always 24 bytes (three 8-byte words), however long the text is. The text itself lives on the heap.
02
The three ownership rules
✓
Each value in Rust has an owner — a variable (or a field, or a collection slot).
There can be only one owner at a time.
When the owner goes out of scope, the value is dropped: its destructor runs and its heap memory is freed.
Rule 3 is automatic: the compiler inserts the cleanup at the closing brace of the owner's scope. You can watch it happen by giving a type a Drop implementation that prints a line (traits are Module 07 — here it is just a hook that runs on drop).
rustmain.rs
1234567891011121314151617
structNoisy(&'staticstr);implDropforNoisy{fndrop(&mutself){println!("drop {}",self.0);}}fnmain(){let _a =Noisy("a");{let _b =Noisy("b");println!("inner scope ends");}// _b dropped herelet _c =Noisy("c");println!("main ends");}// _c then _a: reverse order of creation
Outputcompiled & run with real Rust
inner scope ends
drop b
main ends
drop c
drop a
Your turn
Add drop(_c); (the standard std::mem::drop) just before the last println!. Where does drop c appear now?
This is RAII
Tying cleanup to scope is called RAII (resource acquisition is initialisation), the same idea as C++ destructors. It is not only for memory: files close, locks unlock and network sockets shut when their owner is dropped. You never write a finally block in Rust.
03
Move semantics
✓
What should let s2 = s1; do when s1 is a String? Copying the heap bytes would be slow and hidden. Copying only the pointer would leave two owners, and both would free the same buffer at the end of the scope — a double free. Rust does neither: it moves ownership. The stack handle is copied to s2, and s1 is marked as no longer valid. Only s2 will free the buffer.
VisualizeTracing a moveStep 1 / 6
1fn main(){
2 let s1 = String::from("hello");
3 let s2 = s1;
4 println!("{s2}");
5 let s3 = s2;
6 println!("{s3}");
7}
Line 2
s1 owns a heap buffer holding "hello".
Variables now
s1
"hello" (owner)
All 6 steps as a table
Step
Line
What happened
Variables now
1
2
s1 owns a heap buffer holding "hello".
s1 = "hello" (owner)
2
3
The pointer/len/capacity are copied into s2 and ownership moves with them. s1 is now invalid: the compiler rejects any later use of it.
s1 = (moved)s2 = "hello" (owner)
3
4
s2 is the owner, so reading it is fine.
4
5
Ownership moves again. s2 is invalid too.
s2 = (moved)s3 = "hello" (owner)
5
6
Print through the current owner.
6
7
End of scope. Only s3 is a live owner, so the buffer is freed exactly once. Moved-from variables drop nothing.
error[E0382]: borrow of moved value: `s1`
--> main.rs:4:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{s1}, world");
| ^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
Why the compiler said that
The value moved from s1 to s2 on line 3. After that s1 owns nothing, so reading it on line 4 would be reading memory that s2 now controls. Read the three markers bottom-up: where it was used, where it moved, and why it moved (not Copy).
The fix
Decide what you meant. If both variables need their own text, clone. If you only need to read the value, borrow it (let s2 = &s1;). If you are done with s1, just use s2.
rust
123456
fnmain(){let s1 =String::from("hello");let s2 =&s1;// borrow instead of moveprintln!("{s1}, world");println!("{s2}");}
rustmain.rs
12345678910
fnmain(){let names =vec![String::from("ada"),String::from("grace")];let team = names;// the whole Vec moves; no element is copiedprintln!("{:?}", team);letmut owner =String::from("first");println!("{owner}");
owner =String::from("second");// old value is dropped, new one ownedprintln!("{owner}");}
Outputcompiled & run with real Rust
["ada", "grace"]
first
second
04
Clone vs Copy
✓
.clone() makes a deep copy: a new heap allocation with the same contents, and a second independent owner. It is explicit on purpose — when you see clone() you know memory is being allocated. Small stack-only types are different: they implement the Copy trait, so assignment copies the bits and the original stays valid. There is nothing to free, so two copies cannot cause a double free.
rustmain.rs
1234567891011121314
fnmain(){// Copy types: assignment duplicates, both stay usablelet a =5;let b = a;let p =(1.5,true);let q = p;println!("{a} {b} {:?} {:?}", p, q);// Clone: an explicit deep copy of heap datalet s1 =String::from("hello");letmut s2 = s1.clone();
s2.push_str(" world");println!("{s1} | {s2}");}
Outputcompiled & run with real Rust
5 5 (1.5, true) (1.5, true)
hello | hello world
A type can be Copy only if it owns no resources that need cleanup.
Copy (assignment copies)
Not Copy (assignment moves)
All integers, floats, bool, char
String, Vec<T>, Box<T>, HashMap
Shared references &T
Mutable references &mut T
Tuples and arrays whose elements are all Copy
Tuples or arrays containing any non-Copy element
Your own types with #[derive(Clone, Copy)] (Module 04)
Anything that implements Drop
Do not clone to silence the borrow checker
Adding .clone() makes most E0382 errors go away, which is why beginners sprinkle it everywhere. It is correct when you truly need two independent values. When you only need to read, borrowing is free and clone is wasted allocation. Reviewers notice.
05
Functions that take and return ownership
✓
Passing a value to a function works exactly like assignment: a Stringmoves into the parameter, an i32 is copied. When the function ends, its parameter goes out of scope and is dropped — unless the function hands it back as a return value, which moves ownership out to the caller.
rustmain.rs
1234567891011121314151617181920212223242526272829
fntake(s:String){println!("took {s}");}// s dropped here: its heap memory is freedfnmake()-> String {String::from("fresh")// ownership moves out to the caller}fntake_and_give_back(mut s:String)-> String {
s.push('!');
s // move it back out}fnmain(){let a =String::from("one");take(a);// a moved in; a is invalid from herelet n =7;let doubled =double(n);// i32 is Copy; n is still fineprintln!("{n} -> {doubled}");let b =make();let b =take_and_give_back(b);println!("{b}");}fndouble(x:i32)-> i32 {
x *2}
Outputcompiled & run with real Rust
took one
7 -> 14
fresh!
Error you will hit
E0382: using a value after passing it to a function
error[E0382]: borrow of moved value: `msg`
--> main.rs:8:22
|
6 | let msg = String::from("ship it");
| --- move occurs because `msg` has type `String`, which does not implement the `Copy` trait
7 | shout(msg);
| --- value moved here
8 | println!("sent: {msg}");
| ^^^ value borrowed here after move
|
note: consider changing this parameter type in function `shout` to borrow instead if owning the value isn't necessary
--> main.rs:1:16
|
1 | fn shout(text: String) {
| ----- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
Why the compiler said that
shout declared its parameter as String, so calling it moved msg into the function, where it was dropped at the end. The compiler even suggests the real fix in its note: the function only reads the text, so it has no reason to own it.
The fix
Change the parameter to a borrow, &str, and pass &msg. Giving ownership back through the return value works too, but is clumsy — that clumsiness is exactly why references exist.
A reference&T lets you use a value without taking ownership of it. Creating one is called borrowing. The owner keeps the value, the borrower gets to look at it, and when the reference goes out of scope nothing is dropped — it never owned anything. References are guaranteed by the compiler to point at a valid value for as long as they exist.
rustmain.rs
12345678910111213141516171819202122
fncount_vowels(text:&str)-> usize {
text.chars().filter(|c|"aeiou".contains(*c)).count()}fnlongest_len(words:&Vec<String>)-> usize {letmut best =0;for w in words {// w is &Stringif w.len()> best {
best = w.len();}}
best
}fnmain(){let title =String::from("ownership and borrowing");let words =vec![String::from("move"),String::from("borrow"),String::from("slice")];println!("vowels: {}",count_vowels(&title));println!("longest: {}",longest_len(&words));println!("still mine: {title} / {:?}", words);// nothing moved}
Outputcompiled & run with real Rust
vowels: 7
longest: 6
still mine: ownership and borrowing / ["move", "borrow", "slice"]
A plain &T is a shared (read-only) reference. To change a borrowed value you need a mutable reference, &mut T. Both sides have to agree: the owner must be let mut, and the call site must write &mut, so mutation is visible where it happens.
rustmain.rs
1234567891011121314151617181920
fnadd_bang(s:&mutString){
s.push('!');}fnreset(n:&muti32){*n =0;// * dereferences: write to the i32 itself}fnmain(){letmut s =String::from("done");add_bang(&mut s);add_bang(&mut s);println!("{s}");letmut counter =41;
counter +=1;println!("{counter}");reset(&mut counter);println!("{counter}");}
Outputcompiled & run with real Rust
done!!
42
0
Error you will hit
E0596: mutating through a shared reference
rust
123456789
fnadd_bang(s:&String){
s.push('!');}fnmain(){letmut s =String::from("done");add_bang(&s);println!("{s}");}
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
--> main.rs:2:5
|
2 | s.push('!');
| ^ `s` is a `&` reference, so it cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
1 | fn add_bang(s: &mut String) {
| +++
Why the compiler said that
push changes the string, so it needs &mut String. The function only received &String, a read-only view. That s in main is mut does not matter: what a borrower may do is decided by the kind of reference it was given.
The fix
Take &mut String in the signature and pass &mut s at the call.
rust
123456789
fnadd_bang(s:&mutString){
s.push('!');}fnmain(){letmut s =String::from("done");add_bang(&mut s);println!("{s}");}
07
One mutable XOR many shared
✓
This is the rule the borrow checker enforces, and the one to memorise: at any moment a value may have either one mutable reference, or any number of shared references — never both. Readers can share; a writer needs to be alone. It is exactly the rule that prevents data races, and it also prevents a quieter bug: changing a collection while someone holds a pointer into it.
Error you will hit
E0499: two mutable borrows at once
rust
12345678
fnmain(){letmut s =String::from("hi");let a =&mut s;let b =&mut s;
a.push('!');
b.push('?');println!("{s}");}
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> main.rs:4:13
|
3 | let a = &mut s;
| ------ first mutable borrow occurs here
4 | let b = &mut s;
| ^^^^^^ second mutable borrow occurs here
5 | a.push('!');
| - first borrow later used here
Why the compiler said that
a is still going to be used on line 5, so its mutable borrow is alive when b tries to take a second one on line 4. Two writers to the same value at the same time is precisely what the rule forbids.
The fix
Finish with one mutable borrow before starting the next. Borrows end at their last use, not at the end of the scope, so reordering is often all it takes.
rust
12345678
fnmain(){letmut s =String::from("hi");let a =&mut s;
a.push('!');// last use of alet b =&mut s;// fine: a is finished
b.push('?');println!("{s}");}
Error you will hit
E0502: mutating while a shared borrow is alive
rust
123456
fnmain(){letmut scores =vec![10,20,30];let first =&scores[0];
scores.push(40);println!("first = {first}");}
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
--> main.rs:4:5
|
3 | let first = &scores[0];
| ------ immutable borrow occurs here
4 | scores.push(40);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("first = {first}");
| ----- immutable borrow later used here
Why the compiler said that
This one is not pedantry. push may need more capacity, in which case the Vec allocates a new buffer, copies the elements and frees the old one. first would then point into freed memory. In C++ the same code compiles and is undefined behaviour ("iterator invalidation"); Rust refuses it.
The fix
Copy the value out (let first = scores[0]; — an i32 is Copy), or finish using the reference before mutating.
rust
123456
fnmain(){letmut scores =vec![10,20,30];let first = scores[0];// a copy, not a borrow
scores.push(40);println!("first = {first}");}
rustmain.rs
12345678910111213
fnmain(){letmut data =vec![1,2,3];let r1 =&data;let r2 =&data;// many readers: fineprintln!("{:?} {:?}", r1, r2);// last use of r1 and r2let w =&mut data;// one writer: fine, readers are done
w.push(4);println!("{:?}", w);println!("len {}", data.len());// w is done, the owner can read again}
Outputcompiled & run with real Rust
[1, 2, 3] [1, 2, 3]
[1, 2, 3, 4]
len 4
Borrows last until their last use (non-lexical lifetimes), not until the closing brace. That is why the mutable borrow on line 8 is allowed.
You have
Can you also take &T?
Can you also take &mut T?
Can the owner read / write?
Only the owner
Yes
Yes (owner must be mut)
Yes / yes
One or more live &T
Yes, any number
No — E0502
Read yes, write no
One live &mut T
No — E0502
No — E0499
No — go through the reference
08
Dangling references and slices
✓
A dangling reference points at memory that has already been freed. The classic way to create one is to return a reference to a local variable: the local is dropped when the function returns, and the caller is left holding a pointer to nothing. Rust rejects this at compile time.
Error you will hit
E0106: returning a reference to nothing
rust
12345678
fndangle()->&String{let s =String::from("temporary");&s
}fnmain(){println!("{}",dangle());}
error[E0106]: missing lifetime specifier
--> main.rs:1:16
|
1 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
1 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn dangle() -> &String {
1 + fn dangle() -> String {
|
Why the compiler said that
A returned reference must borrow from something that outlives the call. This function has no reference parameters, so the only thing it could borrow from is its own local s — which is dropped at the closing brace. The help line says it plainly: "there is no value for it to be borrowed from". Lifetimes are the full story, in Module 08.
The fix
Return the owned String. Ownership moves out to the caller and nothing is freed early.
A slice is a reference to a contiguous run of elements: a pointer plus a length. &str is a slice of UTF-8 text; &[T] is a slice of an array or Vec. You make one with a range: &s[0..5], &v[2..], &v[..]. Because a slice is a borrow, the borrowing rules keep it valid — you cannot clear a string while a slice of it is still in use.
rustmain.rs
12345678910111213141516171819202122232425262728
fnfirst_word(s:&str)->&str{for(i, b)in s.bytes().enumerate(){if b == b' '{return&s[..i];}}
s
}fnsum(values:&[i32])-> i32 {letmut total =0;for v in values {
total += v;}
total
}fnmain(){let owned =String::from("hello brave world");let literal ="single";// string literals are already &strprintln!("{}",first_word(&owned));// &String coerces to &strprintln!("{}",first_word(literal));println!("{}",&owned[6..11]);let nums =vec![4,8,15,16,23,42];let arr =[1,2,3];println!("{} {} {}",sum(&nums),sum(&nums[1..3]),sum(&arr));}
Outputcompiled & run with real Rust
hello
single
brave
108 23 6
Your turn
Write fn largest(values: &[i32]) -> i32 and call it with the whole nums vector and with the slice &nums[..3].
Take &str and &[T] in function parameters
A parameter typed &str accepts a string literal, a &String and a slice of either. A parameter typed &String accepts only the second. The same goes for &[T] over &Vec<T>. Prefer the slice type — Clippy even warns about &Vec<T> parameters.
String slice indexes are byte offsets
&s[0..2] counts bytes, not characters. Slicing through the middle of a multi-byte character such as é panics with byte index 1 is not a char boundary. Use chars() when you need characters (Module 05).
Owner
The variable responsible for a value. When it goes out of scope, the value is dropped.
Move
Transferring ownership by assignment, passing or returning. The source can no longer be used.
Copy
A trait for small stack-only types; assignment duplicates the bits and the source stays valid.
Clone
An explicit deep copy, usually a new heap allocation: .clone().
Drop
Freeing a value when its owner goes out of scope. Happens in reverse order of declaration.
Borrow
Creating a reference to a value without taking ownership.
Shared reference
&T: read-only access. Any number may exist at once.
Mutable reference
&mut T: read-write access. Only one may exist, with no shared references alongside.
Borrow checker
The compiler pass that enforces the ownership and borrowing rules.
Slice
A reference to a contiguous part of a collection: &str, &[T].
Dangling reference
A pointer to memory that has been freed. Impossible in safe Rust.
Quick check
let a = String::from("x"); let b = a; let c = 5; let d = c; — which variables can still be used afterwards?
String is not Copy, so a moved into b and is invalid. i32 is Copy, so d is a copy and c stays usable.
Quick check
While a shared reference r = &v[0] is still going to be used, you call v.push(1). What happens?
push needs &mut v, and a live shared borrow forbids that. The rule exists because push can reallocate and leave r dangling.
Every value has exactly one owner variable, and when that owner goes out of scope the value is freed — so memory is managed at compile time with no garbage collector and no manual free.
When should I use clone() in Rust?
When you genuinely need two independent copies of heap data, for example to keep an original while modifying a copy. If you only need to read a value, pass a reference (&T) instead; it costs nothing.
Why can I not have two mutable references to the same value in Rust?
Two writers to the same memory at once is a data race in threaded code and a source of invalidated pointers in single-threaded code. Rust rules it out at compile time: one &mut T or any number of &T, never both.
Finish the Rust handbook, then get hired
Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.