Understanding Ownership
The whole Rust thesis: moves, borrowing, and slices — how memory safety happens without a GC.
Part of Rust Interview Mastery · Based on The Rust Programming Language
Ownership & Move Semantics
State Rust's three ownership rules and what they buy you.
- Every value has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
The payoff: memory management with no garbage collector and no manual free. The compiler knows statically who frees what, so deallocation is deterministic — it's just scope rules plus an automatic Drop call. This is the foundation every other Rust feature builds on.
Explain stack vs heap in Rust terms — where do String's parts live?
The stack holds fixed-size values in LIFO order — fast pointer-bump allocation. The heap holds growable data — the allocator finds a slot and hands back a pointer.
String is the canonical split: a small struct on the stack (ptr, len, cap) where ptr points at a heap-allocated UTF-8 buffer. When the String moves or drops, only the stack part is copied/freed around — the heap buffer has exactly one owner, so it's freed exactly once.
What actually happens at let s2 = s1 for a String?
The stack struct (ptr, len, cap) is copied — the heap buffer is not cloned. Then Rust invalidates s1. This shallow-copy-plus-invalidate is a move, not a copy.
Why invalidate: if both bindings stayed live, both would try to free the same heap buffer at scope end — a double-free. Rust's answer is to make the type system enforce single ownership. s1 after the move is a compile error, not a runtime bug.
Clone vs Copy — what's the contract of each?
Clone is an explicit deep copy: let s2 = s1.clone() duplicates the heap data; both strings stay valid. It's opt-in because heap copying is expensive and should be visible.
Copy is an implicit bitwise duplicate for types that live entirely on the stack — integers, floats, bools, chars, and tuples/arrays of Copy types. Copy types never move; assignment just copies. A type can't be both Copy and Drop — if it owns a resource needing cleanup, it can't be trivially duplicated.
How does ownership interact with function calls?
Passing a value to a function moves or copies it, same rules as assignment:
fn takes(s: String) { /* s dropped here */ }
fn gives() -> String { String::from("hi") } // ownership moves out
let s = String::from("a");
takes(s);
// s is invalid now
So an owned parameter is consumed; to get the value back you'd return it (the book shows the awkward tuple-return workaround). The real solution is references — the next section.
References & Borrowing
What problem do references solve that ownership alone can't?
Ownership alone makes every function call a hostage exchange — pass a String to len(s) and you lose it. References let functions borrow: fn len(s: &String) -> usize reads the value without owning it, and the borrow ends when the function returns.
No ownership moves, no drop happens, no clones needed. Borrowing is what makes Rust's ownership usable day to day.
State the borrowing rules precisely. Why does the compiler enforce them?
At any point in the program you may have either:
- any number of immutable references &T, or
- exactly one mutable reference &mut T.
Never both at once. This is data-race prevention at compile time: a data race needs two writers (or a reader + writer) aliased and uncoordinated — the borrow rules make that state unrepresentable. It also prevents iterator-invalidation: nobody can mutate a Vec you're iterating over, because iteration holds an & borrow.
Why can't Rust return a reference to a local variable?
fn dangle() -> &String {
let s = String::from("x");
&s // ERROR: returns ref to local
}
When dangle returns, s is dropped and its memory freed — the reference would point at deallocated memory. The compiler rejects it outright.
The fix is to return the owned value (-> String) or a reference tied to an input lifetime (chapter 10). Rust's entire lifetime system exists to prove statements like "this reference outlives its referent" at compile time.
What is NLL — non-lexical lifetimes — in practical terms?
NLL means a borrow ends at its last use, not at the closing brace of its scope:
let mut s = String::from("hi");
let r1 = &s; let r2 = &s;
println!("{r1} {r2}"); // r1/r2's borrows END here
let r3 = &mut s; // OK — no immutable borrows still live
Before NLL (Rust 2015-ish), a borrow lived till scope end and this rejected. NLL is why modern Rust lets "obviously fine" code compile — the borrow checker analyzes actual use, not curly braces.
"Cannot borrow `x` as mutable because it is also borrowed as immutable" — what do you do?
The error means a live & borrow exists while you're asking for &mut. The fix depends on intent:
- Shrink the immutable borrow's life: use it (print, compute) before mutating — NLL usually makes this free.
- Clone the data you were borrowing if you genuinely need it post-mutation.
- Restructure: index-based loops, split_at_mut, or collecting first (let keys: Vec<_> = map.keys().cloned().collect()) when mutating what you're iterating.
- For shared mutation across owners: interior mutability (RefCell, Mutex) — chapter 15's answer.
Don't just sprinkle .clone() — decide who should own the data.
Slices
What is a slice, and why is &str not the same thing as String?
A slice is a view into a contiguous sequence you don't own: &[T] = (pointer, length). &str is the string-flavored slice — a view into UTF-8 bytes.
String owns a heap buffer; &str borrows a window into someone else's data — a String, a string literal in the binary, a subrange. That's why &str can appear in so many places: it's the universal "read-only text" currency.
How do string slices prevent the index-desync bug the book describes?
The book's first_word example shows the failure: returning a byte index usize into a String, then mutating the String, leaves the index pointing at stale bytes.
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b' ' { return &s[0..i]; }
}
&s[..]
}
Returning &str ties the result's lifetime to the input — you can't hold the slice and mutate the String, because mutation needs &mut and the slice holds &. The desync class of bug is deleted by the type system.
Why do function signatures prefer &str over &String?
fn first_word(s: &str) -> &str // accepts String AND &str AND literals
&String only accepts &String. &str accepts everything via deref coercion: &String auto-converts to &str, &"literal" already is &str, and so is &s[..]. One signature serves every string-ish caller — this is the standard API guideline for read-only text parameters.
What is the type of a string literal, and where does it live?
String literals are &'static str — a slice pointing into the compiled binary's read-only data, valid for the entire program duration. No allocation, no owner needed at runtime.
That's why let s = "hi" needs no mut to stay immutable, why literals are cheap to pass around, and why 'static lifetime bounds (chapter 10) accept them.