Why Ownership Matters
Ownership is Rust's most distinctive feature and the foundation of its memory safety guarantees. Instead of using a garbage collector (like Java/Go) or manual memory management (like C/C++), Rust uses a system of ownership with rules that the compiler checks at compile time. No runtime cost is incurred.
The Three Ownership Rules
- Each value has exactly one owner — the variable that holds it
- There can only be one owner at a time — when ownership transfers, the old owner is invalidated
- When the owner goes out of scope, the value is dropped — memory is freed automatically
Stack vs Heap
Understanding where data lives in memory is essential to understanding ownership:
| Stack | Heap |
|---|---|
| Fixed-size data (i32, f64, bool, etc.) | Dynamic-size data (String, Vec, etc.) |
| Very fast (push/pop) | Slower (allocation/deallocation) |
| Automatically cleaned up | Needs ownership tracking |
| Data is copied (cheap) | Data is moved (pointer transferred) |
Move Semantics
When you assign a heap-allocated value to another variable, Rust moves ownership. The original variable becomes invalid. This prevents double-free bugs.
fn main() {
// Heap-allocated String
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{s1}"); // ERROR: value borrowed after move
println!("{s2}"); // OK — s2 owns the data
// What happens in memory:
// s1 → [ptr, len, cap] → "hello" on heap
// After move:
// s1 → (INVALID)
// s2 → [ptr, len, cap] → "hello" on heap
// Moves happen on assignment, function calls, and returns
let s3 = String::from("world");
takes_ownership(s3);
// println!("{s3}"); // ERROR: s3 was moved into the function
}
fn takes_ownership(s: String) {
println!("Got: {s}");
} // s is dropped here, memory freed
Common Pitfall: Accidental Moves
New Rust developers often get "value used after move" errors. The fix is usually to clone the data, borrow it with a reference, or restructure the code so the original owner isn't needed after the transfer.
The Copy Trait
Simple stack-only types implement the Copy trait. When copied, the original
remains valid because copying is cheap (just a bitwise copy on the stack).
fn main() {
// Integers implement Copy
let x = 5;
let y = x; // x is COPIED, not moved
println!("x={x}, y={y}"); // Both valid!
// Types that implement Copy:
// - All integer types (i32, u64, etc.)
// - bool
// - char
// - f32, f64
// - Tuples of Copy types: (i32, bool) ✅
// - Tuples with non-Copy: (i32, String) ❌
// Types that do NOT implement Copy:
// - String (heap-allocated)
// - Vec (heap-allocated)
// - Any type with a Drop implementation
}
// Stack-only types are copied into functions
fn makes_copy(n: i32) {
println!("Got: {n}");
}
fn main_copy_demo() {
let num = 42;
makes_copy(num);
println!("Still valid: {num}"); // OK — i32 is Copy
}
Clone: Explicit Deep Copy
fn main() {
let s1 = String::from("hello");
// Clone creates a deep copy (new heap allocation)
let s2 = s1.clone();
// Both are valid because s2 has its own heap data
println!("s1={s1}, s2={s2}");
// Clone can be expensive for large data!
let big_vec = vec![0; 1_000_000];
let big_copy = big_vec.clone(); // Copies 1M elements
println!("Lengths: {} {}", big_vec.len(), big_copy.len());
}
Ownership and Functions
fn main() {
let s = String::from("hello");
// Passing to a function moves ownership
let len = calculate_length_takes(s);
// s is no longer valid here
// Better: return ownership back
let s2 = String::from("world");
let (s2, len2) = calculate_and_return(s2);
println!("{s2} has length {len2}");
// Best: use references (next topic!)
}
fn calculate_length_takes(s: String) -> usize {
s.len()
} // s dropped here
fn calculate_and_return(s: String) -> (String, usize) {
let len = s.len();
(s, len) // Return ownership
}
Key Takeaways
- ✅ Each value in Rust has exactly one owner — when the owner goes out of scope, memory is freed
- ✅ Assignment of heap data moves ownership, invalidating the original variable
- ✅ Stack-only types (integers, bool, char) implement Copy and are cheaply duplicated
- ✅ Use
.clone()for explicit deep copies when you need both values - ✅ Ownership eliminates entire classes of memory bugs at compile time with zero runtime cost