On this page
Ownership: the heart of Rust
Ownership: Rust's central concept
The ownership system is what distinguishes Rust from all other programming languages. It is the mechanism that allows Rust to guarantee memory safety without a garbage collector (GC).
To understand why ownership exists, we first need to understand the problem it solves.
The problem: memory management
All programs must manage the memory they use. There are two main approaches:
Garbage collection (GC): The language automatically tracks which memory is no longer used and frees it. Advantage: simple for the programmer. Disadvantage: unpredictable pauses, higher memory usage, not suitable for real-time systems.
Manual management: The programmer is responsible for
malloc/freeornew/delete. Advantage: full control. Disadvantage: use-after-free, double-free, memory leaks, buffer overflows.
Rust takes a third path: the compiler verifies ownership rules at compile time and generates code that frees memory automatically at exactly the right moment, with no runtime overhead whatsoever.
The three rules of Ownership
Rust has exactly three rules:
- Each value in Rust has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
These rules sound simple but have profound implications.
Stack vs Heap
To understand ownership, you need to be clear about the difference between stack and heap:
Stack:
- LIFO (last in, first out) memory
- Size known at compile time
- Instantaneous allocation and deallocation
- Very fast
- Examples:
i32,f64,bool,char,[i32; 5]
Heap:
- Dynamic memory, can grow or shrink
- The OS finds a free space and returns a pointer
- Slower than the stack
- Requires explicit management (in C/C++) or ownership (in Rust)
- Examples:
String,Vec<T>,Box<T>
fn main() {
// On the stack: fixed size, copied automatically
let x: i32 = 5; // 4 bytes on the stack
let y = x; // Trivial copy
println!("x={x}, y={y}"); // Both valid
// On the heap: String has 3 parts on the stack:
// - pointer to heap (8 bytes)
// - current length (8 bytes)
// - capacity (8 bytes)
// Plus the content on the heap
let s1 = String::from("hello");
println!("s1 points to {} bytes on the heap", s1.capacity());
}Move semantics
When you assign a heap value to another variable, Rust performs a move, not a copy:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// At this point:
// - s2 is the owner of the String "hello"
// - s1 is NO LONGER valid — the compiler knows this
println!("{s2}"); // OK
// println!("{s1}"); // Error: "value borrowed here after move"
}Why not just copy? If Rust automatically copied, we would have two owners of the same heap data. When both went out of scope, the same memory would be freed twice — a "double free" error that causes undefined behavior.
Instead of copying, Rust invalidates s1 when it is moved to s2. Only s2 frees the memory when it goes out of scope.
Clone: explicit heap copy
When you genuinely need a full copy of a heap value, use .clone():
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy — explicit
println!("s1 = {s1}"); // OK: s1 is still valid
println!("s2 = {s2}"); // OK: s2 is an independent copy
// Modifying s2 does not affect s1
let mut s3 = s1.clone();
s3.push_str(" world");
println!("s1 = {s1}"); // "hello"
println!("s3 = {s3}"); // "hello world"
}The key point: .clone() is expensive and explicit. When you see it in code, you know a heap copy is happening.
The Copy trait
Types that are "cheap to copy" implement the Copy trait. For these types, assignment always creates a copy rather than a move:
fn main() {
// Copy types: assignment copies, not moves
let a: i32 = 10;
let b = a; // Copy
let c: f64 = 3.14;
let d = c; // Copy
let e: bool = true;
let f = e; // Copy
let g: char = 'Z';
let h = g; // Copy
// All still valid
println!("{a} {b} {c} {d} {e} {f} {g} {h}");
// References &T are Copy (the reference is copied, not the data)
let s = String::from("text");
let r1: &String = &s;
let r2 = r1; // Copies the reference
println!("{r1} {r2}"); // Both valid
}A type can implement Copy only if all of its fields also implement Copy. String cannot be Copy because it manages heap memory.
Ownership and functions
Ownership follows the same rules when passing values to functions:
fn main() {
let s = String::from("Rust");
// Passing s MOVES ownership to the function
print_string(s);
// println!("{s}"); // Error: s was moved
// To keep ownership, the function can return it
let s2 = String::from("Ownership");
let s2 = return_ownership(s2);
println!("Got it back: {s2}");
// This is verbose — references (next lesson) solve this elegantly
}
fn print_string(string: String) {
println!("{string}");
} // string is dropped here
fn return_ownership(string: String) -> String {
println!("{string}");
string // Returns ownership to the caller
}Drop: automatic memory deallocation
When an owner goes out of scope, Rust automatically calls the value's destructor, implemented through the Drop trait:
struct ImportantResource {
name: String,
}
impl Drop for ImportantResource {
fn drop(&mut self) {
println!("Releasing resource: {}", self.name);
}
}
fn main() {
let r1 = ImportantResource { name: String::from("DB Connection") };
let r2 = ImportantResource { name: String::from("Log file") };
println!("Resources created");
// When they go out of scope, Rust automatically calls drop()
// in reverse order of creation: r2 first, then r1
println!("Ending main...");
} // r2.drop() then r1.drop() — no programmer intervention neededThis pattern, called RAII (Resource Acquisition Is Initialization), guarantees resources are always released, even if an error condition occurs. It is impossible to forget to close a database connection in Rust.
The ownership system can feel restrictive at first, but it is the foundation of all of Rust's safety guarantees. The next lesson teaches you references and borrowing — the elegant way to use data without transferring ownership.
fn main() {
// String on the heap — has an owner
let s1 = String::from("hello");
// MOVE: s1 is moved to s2, s1 is no longer valid
let s2 = s1;
// println!("{s1}"); // Error: value moved
println!("s2 = {s2}");
// CLONE: deep copy of the heap
let s3 = s2.clone();
println!("s2 = {s2}, s3 = {s3}");
// Copy types live on the stack: copied automatically
let x: i32 = 5;
let y = x; // Copy, not move
println!("x = {x}, y = {y}"); // Both valid
// Ownership and functions
let string = String::from("world");
take_ownership(string);
// println!("{string}"); // Error: moved
let number = 42_i32;
make_copy(number);
println!("number is still {number}"); // Valid: i32 is Copy
}
fn take_ownership(s: String) {
println!("I took ownership of: {s}");
} // s is dropped here
fn make_copy(n: i32) {
println!("I have a copy: {n}");
}
Sign in to track your progress