What is Ownership?
Ownership Rules
Rust’s ownership system answers one central question:
Who is responsible for this value right now?
More precisely:
For a given value (and any heap memory it manages), which binding is responsible for it?
The Three Ownership Rules
-
Each value has exactly one owner at a time.
The owner is the binding (name) currently responsible for that value. -
Ownership can move to another binding.
After a move, the previous binding can no longer be used. -
When the owner goes out of scope, the value is dropped.
- For stack-only values (
i32,bool, etc.), this is trivial. - For heap-backed values (
String,Vec<T>), this frees the heap memory.
- For stack-only values (
What does “one owner at a time” actually mean?
It does NOT mean:
“Only one variable in the program can have this number.”
It means:
One specific value instance is owned by one specific binding at a time.
For example:
let x = 5;
let y = 5;
Here:
xowns its5yowns its5
Even though the values look identical, they are separate value instances.
For simple scalar values, ownership doesn’t feel dramatic.
It becomes important when heap memory is involved.
Where ownership really matters (heap-backed values)
let s1 = String::from("hello");
let s2 = s1; // ownership moves
// s1 is no longer valid here
Here:
Stringstores data on the heap- When
s2 = s1happens, ownership moves s1is no longer usable
This prevents:
- double-free
- use-after-free
- dangling pointers
Rust ensures that heap memory is freed exactly once — when the final owner goes out of scope.
Clarifying “value” vs “allocation”
When we say “value,” we mean the entire thing.
For String, that includes:
- stack metadata (pointer, length, capacity)
- heap allocation (actual string contents)
Ownership governs the entire structure.
Quick Summary Table
| Concept | Meaning |
|---|---|
| Owner | Binding responsible for a value |
| Move | Transfer ownership to a new binding |
| Drop | Automatic cleanup when owner goes out of scope |
| Heap-backed type | Owns heap memory (String, Vec<T>) |
| Stack-only type | Stored entirely on stack (i32, bool) |
The String Type
So far, we’ve mostly used string literals like "hello".
In Rust, there are two common “string” forms, and they exist for a reason:
&str(string slice) — usually a string literal, known ahead of timeString— a growable string type that can hold text whose size is not known at compile time (often user input)
Key idea: &str vs String
-
A string literal like
"hello"is typically a&'static str:- the text is baked into the compiled program (read-only)
- you borrow it; you don’t “own” or resize it
-
A
Stringowns its contents:- it stores its actual text on the heap
- it can grow/shrink as needed (push, append, etc.)
- it’s commonly used for user input (
read_line) and text building
Example 1 — &str (string literal)
fn main() {
let greeting = "hello"; // greeting: &'static str
println!("{greeting}");
// greeting.push_str(" world"); // ❌ not allowed
}
Why?
"hello"is stored in the program’s binarygreetingis just a reference to that static memory- It cannot be resized or modified
Example 2 — String (owned, heap-backed)
fn main() {
let mut greeting = String::from("hello"); // greeting: String
greeting.push_str(" world");
greeting.push('!');
println!("{greeting}");
}
Here:
String::from("hello")allocates memory on the heapgreetingowns that allocation- Because it’s declared
mut, it can grow and change
More detail: what’s the difference between &str and String?
1) What type is "hello"?
let s: &str = "hello";
This is a string slice that points at some UTF-8 bytes.
For string literals, those bytes live in your program’s read-only memory.
You can take slices of other strings too:
let owned = String::from("hello");
let slice: &str = &owned[0..2]; // "he"
So: &str is a view into some string data owned elsewhere.
2) What type is String::from("hello")?
let s: String = String::from("hello");
This creates an owned string:
sowns a heap allocation containing"hello"- because it owns the bytes, it can modify / grow them
Also ::from is an associated function
Example:
let mut s = String::from("hello");
s.push('!');
println!("{s}");
3) Which one should I use?
- Use
&strwhen you just need to read a string (especially function parameters) - Use
Stringwhen you need to own the text (store it, build it, modify it, accept user input)
A very common Rust pattern is:
fn takes_str(s: &str) {
println!("{s}");
}
fn main() {
let owned = String::from("hello");
takes_str(&owned); // String -> &str via borrowing
takes_str("world"); // literal is already &str
}
Variable Scope and Memory Management
Scope itself is a familiar idea from most programming languages, so I won’t over-explain it here.
If you want the Rust Book’s explanation, see:
https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#variable-scope
For memory/allocation, we already covered the stack vs heap model (and why it matters in Rust) earlier in this chapter.
If you want the official Rust Book section as a reference, see:
https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#memory-and-allocation
Variable Interactions
Similarly, in previous chapters we’ve already touched on “copy vs move” a few times.
For the official explanation + the best visuals, refer to:
https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#variables-and-data-interacting-with-move
Move vs Clone vs Copy (quick refresher)
Move (default for heap-backed types like String, Vec<T>)
let s1 = String::from("hello");
let s2 = s1; // move (s1 invalid)
- Ownership transfers to
s2(prevents double-free / use-after-free)
Clone (explicit deep copy)
let s1 = String::from("hello");
let s2 = s1.clone();
- Heap data is duplicated (can be expensive)
Copy (stack-only types)
let x = 5;
let y = x; // copy (x still valid)
- Trivial bitwise copy (
Copytypes like integers, bool, char, etc.)
Reassignment drops the old value
let mut s = String::from("hello");
s = String::from("ahoy"); // old value dropped
Ownership and Functions
Passing a value to a function follows the same ownership rules as assignment.
- If the type implements
Copy, the value is copied. - If it does not implement
Copy, ownership moves.
Passing ownership into a function
fn main() {
let s = String::from("hello");
takes_ownership(s); // move occurs here
let x = 5;
makes_copy(x); // copy occurs here
}
fn takes_ownership(some_string: String) {
println!("{some_string}");
} // some_string goes out of scope → drop is called
fn makes_copy(some_integer: i32) {
println!("{some_integer}");
}
Key idea:
Stringdoes not implementCopy→ ownership moves.i32does implementCopy→ value is duplicated.
Why does “nothing special happen” for s at the end of main?
s at the end of main?In the book it says:
“Because s’s value was moved, nothing special happens.”
What does that mean?
If s had not been moved, then when main ended:
swould go out of scopedropwould be called- the heap memory would be freed
However, in this example:
takes_ownership(s);
Ownership moved into takes_ownership.
That function already dropped the String when it ended.
So by the time main finishes:
sis no longer valid- there is nothing left to drop
In other words:
The value was already cleaned up earlier.
That’s what “nothing special happens” means.
Return Values and Ownership
Ownership can also move when a function returns a value.
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // moved to caller
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // moved back to caller
}
Ownership always follows the same rule:
- assignment moves
- passing into a function moves
- returning from a function moves
There is no special exception for return values.
Ownership transfer is consistent.
To make this idea more concrete, consider the following pattern:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}
Here:
s1is moved intocalculate_length- the function returns
(String, usize) - ownership of the
Stringmoves back intos2
Question: Why no type annotation here?
In this line:
let (s2, len) = calculate_length(s1);
You might wonder whether something like this is required:
let (s2: String, len: usize) = calculate_length(s1);
The answer is: no.
Rust infers the types from the function signature.
Because the function is defined as:
fn calculate_length(s: String) -> (String, usize)
Rust already knows:
- the first element is
String - the second element is
usize
Tuple destructuring simply binds names to each returned value. Type inference fills in the rest automatically.
Why returning ownership feels awkward
But notice what’s happening in calculate_length:
- take ownership
- use the value
- give ownership back
This ceremony exists only because ownership was transferred into the function.
In many cases, we don’t actually want the function to own the value.
We just want it to temporarily use it.
That is exactly the problem that references solve.
Instead of transferring ownership, we can allow a function to borrow a value.
That’s what we’ll discuss next.