Every value in Rust has exactly one owner at a time, and when the owner goes away, the value is automatically cleaned up.
Let’s build this step by step.
1️⃣ The three ownership rules
Rust enforces these at compile time:
- Each value has one owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
That’s it—but these rules have powerful consequences.
2️⃣ What is an “owner”?
The owner is the variable responsible for freeing the memory.
let s = String::from("hello");
sowns theString- The heap memory for
"hello"belongs tos
When s goes out of scope:
} // s is dropped here → memory freed
No free, no delete, no GC.
3️⃣ Move semantics (ownership transfer)
In Rust, assigning or passing a value often moves ownership.
let a = String::from("hi");
let b = a;
What happens?
- Ownership moves from
a→b ais now invalid
println!("{}", a); // ❌ compile-time error
Why this matters
In C++, both a and b could point to the same memory → double free or use-after-free risk.
Rust avoids this by making ownership exclusive.
4️⃣ Copy types vs move types
Some types are cheap and safe to copy:
let x = 5;
let y = x; // copy
println!("{}", x); // OK
These are types that implement Copy:
- integers
- floats
bool- small fixed-size structs with no heap data
Heap-owning types like String, Vec<T>, Box<T> are not Copy.
5️⃣ Borrowing: using without owning
What if you want to use a value without taking ownership?
Immutable borrow
fn len(s: &String) -> usize {
s.len()
}
let s = String::from("hello");
let l = len(&s); // borrow
println!("{}", s); // still valid
Rules:
- Many immutable borrows allowed
- No mutation allowed while borrowed
Mutable borrow
fn push_x(s: &mut String) {
s.push('x');
}
let mut s = String::from("hi");
push_x(&mut s);
Rules:
- Only one mutable borrow at a time
- No other borrows (mutable or immutable) while it exists
This prevents data races and aliasing bugs.
6️⃣ Why Rust forbids multiple mutable borrows
This code is illegal:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ❌
Why?
- Two mutable references could modify the same memory unpredictably
- In multithreaded code, this would be a data race
Rust prevents this at compile time, even in single-threaded code.
7️⃣ Ownership + borrowing prevent dangling references
This is impossible in safe Rust:
let r;
{
let s = String::from("hello");
r = &s; // borrow
} // s dropped here
println!("{}", r); // ❌
Rust sees that r would outlive s and refuses to compile.
This is how Rust prevents dangling references.
8️⃣ Ownership across function boundaries
Taking ownership
fn consume(s: String) {
println!("{}", s);
}
let s = String::from("hi");
consume(s);
println!("{}", s); // ❌ s moved
Borrowing instead
fn read(s: &String) {
println!("{}", s);
}
let s = String::from("hi");
read(&s);
println!("{}", s); // OK
The function signature tells you exactly what happens to ownership.
9️⃣ Ownership enables safe concurrency
Because:
- Only one owner
- Mutable access is exclusive
Rust can guarantee:
No data races in safe Rust
Sharing across threads must be explicit:
Arc<T>for shared ownershipMutex<T>/ atomics for mutation
10️⃣ Mental model (simple & practical)
Think in questions:
- Who owns this value?
- Am I moving it or borrowing it?
- Is this borrow shared (
&) or exclusive (&mut)? - Does the owner live long enough?
If those answers are clear, Rust code usually “just works”.
One-sentence summary
Ownership is Rust’s compile-time system that ensures each piece of memory has exactly one responsible owner, enabling automatic cleanup, preventing use-after-free, data races, and double frees—without a garbage collector.