Ownership is Rust's core memory management model — every value has exactly one owner, and when the owner goes out of scope, the value is automatically dropped (freed). No garbage collector. No manual free. Enforced entirely at compile time.
Borrowing lets you use a value without taking ownership.
You can have either any number of immutable references
(&T)
or exactly one mutable reference
(&mut T)
— never both at the same time. The borrow checker enforces this statically.
Single Owner
Each value in Rust has exactly one variable that owns it at any given time.
Move on Assignment
When ownership is assigned to another variable, the original variable is invalidated — this is a move. Types that implement Copy are the exception.
Drop on Scope Exit
When the owner goes out of scope, Rust automatically calls drop(), freeing heap memory. Zero runtime cost.
String) cannot have two owners — assigning s1 to s2 transfers ownership instead of copying, so the original variable is invalidated.
// String is heap-allocated → ownership MOVES, not copies fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is MOVED into s2 // println!("{}", s1); // ❌ ERROR: value borrowed after move println!("{}", s2); // ✅ s2 is the owner now }
i32, bool, f64, char) implement Copy — assignment silently duplicates the bits, so both x and y stay valid.
// Scalar types (i32, bool, f64, char) implement Copy — stack-only fn main() { let x: i32 = 42; let y = x; // COPY — x is still valid println!("{} {}", x, y); // ✅ both valid }
.clone() explicitly — Rust never deep-copies silently, so the cost is always visible in the source.
// .clone() deep-copies heap data — explicit, intentional cost fn main() { let s1 = String::from("world"); let s2 = s1.clone(); // deep copy of heap data println!("{} {}", s1, s2); // ✅ both valid, both own separate memory }
&String when a function only needs to read, and &mut String when it needs to mutate — neither takes ownership, so the caller keeps using the value afterwards.
// Immutable borrow — many readers, no mutation fn print_len(s: &String) { // borrows, does NOT take ownership println!("len = {}", s.len()); } // Mutable borrow — exclusive writer fn push_str(s: &mut String) { s.push_str(", world"); } fn main() { let mut s = String::from("hello"); print_len(&s); // immutable borrow push_str(&mut s); // mutable borrow println!("{}", s); // ✅ "hello, world" }
r3) cannot coexist with active immutable borrows (r1, r2) on the same value.
fn main() { let mut s = String::from("hello"); let r1 = &s; // immutable borrow ✅ let r2 = &s; // another immutable borrow ✅ let r3 = &mut s; // ❌ cannot borrow as mutable — r1, r2 still active println!("{} {} {}", r1, r2, r3); }
String struct itself vs the heap address its internal pointer points to — proves a String is a stack-allocated struct (ptr, len, cap) pointing into the heap.
// time for ownership #[allow(dead_code)] pub fn how_stack_heap_looks(){ let s1 = String::from("hello"); println!("address of s1 (stack struct) : {:p}", &s1); println!("ptr inside s1 (heap address) : {:p}", s1.as_ptr()); }
&s1prints the address of theStringstruct on the stack — this struct is just 24 bytes: a pointer, a length, and a capacity.s1.as_ptr()prints the address that the struct's internal pointer holds — this is where the actual bytesh e l l olive, on the heap.- These two addresses will always be different — one is "where the box is", the other is "where the box points to".
.clone() allocates a brand-new heap block (two different as_ptr() addresses), while a move (s2 = s1) transfers the same heap block — s2.as_ptr() equals what s1.as_ptr() was, proving no copy happened.
#[allow(dead_code)] pub fn compare_stack_heap_clone_version() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("=== CLONE VERSION (s2 = s1.clone()) ==="); println!("STACK HEAP"); println!("┌──────────────────────┐"); println!("│ s1 @ {:p} │", &s1 as *const String); println!("│ ptr ──────────────┼──────► ┌──────────────────┐"); println!("│ len = {:<13} │ │ {:p} │", s1.len(), s1.as_ptr()); println!("│ cap = {:<13} │ │ h e l l o │", s1.capacity()); println!("└──────────────────────┘ └──────────────────┘"); println!("┌──────────────────────┐"); println!("│ s2 @ {:p} │", &s2 as *const String); println!("│ ptr ──────────────┼──────► ┌──────────────────┐ (different heap block)"); println!("│ len = {:<13} │ │ {:p} │", s2.len(), s2.as_ptr()); println!("│ cap = {:<13} │ │ h e l l o │", s2.capacity()); println!("└──────────────────────┘ └──────────────────┘"); } #[allow(dead_code)] pub fn compare_stack_heap_move_version() { let s1 = String::from("hello"); let s1_stack_addr = &s1 as *const String; let s1_heap_addr = s1.as_ptr(); let s1_len = s1.len(); let s1_cap = s1.capacity(); let s2 = s1; // s1 moved, can't use s1 after this println!("=== MOVE VERSION (s2 = s1) ==="); println!("STACK HEAP"); println!("┌──────────────────────┐"); println!("│ s1 @ {:p} │ (s1 moved — no longer valid)", s1_stack_addr); println!("│ ptr ──────────────┼──────► ┌──────────────────┐"); println!("│ len = {:<13} │ │ {:p} │", s1_len, s1_heap_addr); println!("│ cap = {:<13} │ │ h e l l o │", s1_cap); println!("└──────────────────────┘ └──────────────────┘"); println!("┌──────────────────────┐"); println!("│ s2 @ {:p} │", &s2 as *const String); println!("│ ptr ──────────────┼──────► ┌──────────────────┐ (SAME heap block as s1)"); println!("│ len = {:<13} │ │ {:p} │", s2.len(), s2.as_ptr()); println!("│ cap = {:<13} │ │ h e l l o │", s2.capacity()); println!("└──────────────────────┘ └──────────────────┘"); } #[allow(dead_code)] pub fn compare_stack_heap(){ compare_stack_heap_clone_version(); println!("------------------------------------------"); compare_stack_heap_move_version(); }
- Clone:
s1ands2each get their own 24-byte stack struct AND their own heap allocation —s1.as_ptr() != s2.as_ptr(). Two separate "hello" strings exist in memory. - Move:
s2.as_ptr()equals the heap address thats1used to point to. The bytesh e l l onever moved — only the ownership (the stack struct's bookkeeping) was transferred tos2. - This is why move is O(1) — it's just copying 24 bytes (ptr/len/cap) on the stack, never touching the heap data.
s1_stack_addr,s1_heap_addretc. are captured before the move becauses1becomes invalid right afterlet s2 = s1;.
#[allow(dead_code)] pub fn ownership_with_3_rules() { // ---- Rule 1: Each value has one owner ---- let s1 = String::from("hello"); println!("Rule 1: s1 is the owner of \"hello\" -> {}", s1); // ---- Rule 2: Only one owner at a time ---- let s2 = s1; // ownership moves from s1 to s2 // println!("{}", s1); // ❌ ERROR: s1 was moved, no longer valid println!("Rule 2: ownership moved -> s2 = {}", s2); // ---- Rule 3: When owner goes out of scope, value is dropped ---- { let s3 = String::from("world"); println!("Rule 3: s3 is alive inside this scope -> {}", s3); } // s3 goes out of scope here -> heap memory dropped/freed // println!("{}", s3); // ❌ ERROR: s3 does not exist outside this scope println!("Rule 3: s3 has been dropped, cannot use it here"); }
- Rule 1:
s1is the single owner of the heap-allocated"hello"right after creation. - Rule 2:
let s2 = s1;moves ownership — the compiler markss1as "moved out" in its borrow-check graph. Any later use ofs1is a compile-time error, not a runtime one. - Rule 3: the inner
{ }block creates a new scope. When it ends,s3'sDrop::drop()runs automatically, freeing"world"'s heap memory — deterministically, with zero GC.
i32 values after assignment — proves Copy types get their own independent stack slot, unlike a move where the original is invalidated.
#[allow(dead_code)] pub fn copy_vs_move_demo() { // ---- COPY TYPES (i32, bool, f64, char etc.) ---- let x = 5; let y = x; // COPY, not move — i32 is a Copy type println!("addres of both: {:p}\n{:p}", &x, &y); println!("Copy type: x = {}, y = {}", x, y); // ✅ both valid // ---- MOVE TYPES (String, Vec, etc.) ---- let s1 = String::from("hello"); let s2 = s1; // MOVE — s1 is no longer valid // println!("{}", s1); // ❌ ERROR: value borrowed here after move println!("Move type: s2 = {}", s2); }
&xand&yprint two different stack addresses — even thoughy = x, Rust gaveyits own 4-byte slot and bitwise-copied5into it.- Both
xandyremain valid and independent — mutating one (if mutable) would never affect the other. - Contrast with
s2 = s1below it: no new heap allocation happens, ownership of the same heap block just transfers, ands1becomes unusable.
&String) actually is in memory: the reference r itself lives on the stack and stores the address of s1 — it's a pointer to a pointer.
#[allow(dead_code)] pub fn step1_what_is_reference() { let s1 = String::from("hello"); let r = &s1; // r is a REFERENCE to s1 println!("=== WHAT IS A REFERENCE ==="); println!("STACK HEAP"); println!("┌──────────────────────┐"); println!("│ s1 @ {:p} │", &s1 as *const String); println!("│ ptr ──────────────┼──────► ┌──────────────────┐"); println!("│ len = {:<13} │ │ {:p} │", s1.len(), s1.as_ptr()); println!("│ cap = {:<13} │ │ h e l l o │", s1.capacity()); println!("└──────────────────────┘ └──────────────────┘"); println!("┌──────────────────────┐"); println!("│ r @ {:p} │ (r itself lives on stack)", &r as *const &String); println!("│ value = {:p} ───────┼──────► points to s1 (above)", r); println!("└──────────────────────┘"); println!(); println!("s1 still usable : {}", s1); // ✅ s1 valid hai println!("r usable too : {}", r); // ✅ r se bhi access ho raha }
&r as *const &Stringprints the stack address ofritself —ris its own variable, occupying its own stack slot (8 bytes, a pointer).r's value is the address ofs1— i.e.rdoesn't store "hello", it stores "wheres1lives".- Both
s1andrremain valid afterwards — borrowing never moves or invalidates the original. - This is the core mental model: a reference is a non-owning pointer with compiler-enforced lifetime guarantees.
&s1 into a function and print addresses inside the function — proves the function receives a reference to the same heap data, not a copy, and ownership returns to the caller untouched.
#[allow(dead_code)] fn print_len(s: &String) { println!("--- inside print_len() ---"); println!("address of s (param) : {:p}", &s as *const &String); println!("s points to : {:p}", s); // same as &s1 in main println!("s.as_ptr() (heap) : {:p}", s.as_ptr()); // same heap as s1 println!("len via reference : {}", s.len()); } #[allow(dead_code)] pub fn step2_borrow_in_function() { let s1 = String::from("hello"); println!("=== BEFORE FUNCTION CALL ==="); println!("s1 @ {:p}", &s1 as *const String); println!("s1.as_ptr() = {:p}", s1.as_ptr()); print_len(&s1); // borrow — ownership NOT moved println!("=== AFTER FUNCTION CALL ==="); println!("s1 still usable: {}", s1); // ✅ s1 valid hai }
s.as_ptr()insideprint_lenprints the exact same heap address ass1.as_ptr()inmain— no data was duplicated when passing&s1.&s as *const &Stringis the address of the parametersitself — a fresh stack slot holding a pointer tos1.- After the call,
s1is still printable inmain— borrowing a function parameter never transfers ownership.
&mut s1 and call .push_str() inside the function — shows the heap pointer can stay the same or change (reallocation) depending on whether capacity was exceeded.
#[allow(dead_code)] fn push_world(s: &mut String) { println!("--- inside push_world() (BEFORE push) ---"); println!("address of s (param) : {:p}", &s as *const &mut String); println!("s points to : {:p}", s); println!("s.as_ptr() (heap) : {:p}", s.as_ptr()); println!("value via reference : {}", s); s.push_str(", world"); println!("--- inside push_world() (AFTER push) ---"); println!("s.as_ptr() (heap) : {:p}", s.as_ptr()); // same ya different? println!("value via reference : {}", s); } #[allow(dead_code)] pub fn step3_mutable_borrow() { let mut s1 = String::from("hello"); println!("=== BEFORE FUNCTION CALL ==="); println!("s1 @ {:p}", &s1 as *const String); println!("s1.as_ptr() = {:p}", s1.as_ptr()); println!("s1.capacity() = {}", s1.capacity()); println!("value = {}", s1); push_world(&mut s1); // mutable borrow println!("=== AFTER FUNCTION CALL ==="); println!("s1.as_ptr() = {:p}", s1.as_ptr()); println!("s1.capacity() = {}", s1.capacity()); println!("value = {}", s1); // ✅ s1 modified }
"hello"has length 5 butString::fromoften allocates capacity 5 too — sopush_str(", world")needs 12 bytes total, exceeding capacity.- When capacity is exceeded, Rust reallocates a bigger heap block, copies the old bytes over, frees the old block — so
s.as_ptr()BEFORE and AFTER push can differ. - This is the hidden cost of
push_str/pushon aStringthat's at capacity — good to know for performance discussions (pre-allocate withString::with_capacity()). - Either way, the mutation is visible back in
mainthroughs1—&mutgives exclusive write access to the same owner's data.
r1 and r2 are immutable borrows that are "dead" after their last use, so r3 = &mut s1 is allowed even though r1/r2 are still in lexical scope.
#[allow(dead_code)] pub fn step4_borrow_rule_violation() { let mut s1 = String::from("hello"); let r1 = &s1; // immutable borrow #1 let r2 = &s1; // immutable borrow #2 — ✅ OK, multiple & allowed // let r3 = &mut s1; // mutable borrow — ❌ ERROR expected println!("r1: {}, r2: {}", r1, r2); let r3 = &mut s1; // mutable borrow — will pass by NLL (Non-Lexical Lifetimes) r3.push_str(", world"); println!("r3: {}", r3); println!("s1: {}", s1); // let r3 = &mut s1; // ❌ ERROR expected // r3.push_str(", world"); // println!("r1: {}, r3: {}", r1, r3); println!("s1: {}", s1); }
- Old Rust (pre-2018, lexical lifetimes):
r1andr2would be considered "alive" until the end of the enclosing{ }block — sor3 = &mut s1would be a hard error. - NLL (current Rust): the borrow checker tracks the actual last use of
r1/r2(theprintln!right after them). Once that line executes, their borrows end —r3can now safely take a mutable borrow. - This is why
println!("r1: {}, r2: {}", r1, r2);must come beforelet r3 = &mut s1;— reorder it after, and the compiler rejects it. - NLL makes Rust feel less restrictive without weakening the actual safety guarantee — it's a smarter scope analysis, not a relaxed rule.
&T OR one &mut T, and (2) references must never dangle — valid_ref cannot escape the scope of the data it points to.
#[allow(dead_code)] pub fn borrowing_reference_with_2_rules() { // ---- Rule 1: Many &T (immutable) OR one &mut T (mutable) at a time ---- let mut s1 = String::from("hello"); let r1 = &s1; // immutable borrow #1 let r2 = &s1; // immutable borrow #2 -> OK, multiple immutable refs allowed println!("Rule 1: multiple immutable refs -> r1 = {}, r2 = {}", r1, r2); // r1, r2 are no longer used after this point (NLL marks them as dead) let r3 = &mut s1; // mutable borrow -> OK now, since r1, r2 are dead r3.push_str(", world"); println!("Rule 1: one mutable ref -> r3 = {}", r3); println!("Rule 1: final value -> s1 = {}", s1); // ---- Rule 2: References must always be valid (no dangling references) ---- let valid_ref; { let s4 = String::from("inner"); valid_ref = &s4; println!("Rule 2: reference is valid inside this scope -> {}", valid_ref); } // s4 goes out of scope here -> heap memory dropped/freed // println!("{}", valid_ref); // ❌ ERROR: s4 does not live long enough, valid_ref would dangle println!("Rule 2: valid_ref cannot be used here, would be a dangling reference"); }
- Rule 1 reuses the NLL pattern:
r1/r2die after theirprintln!, freeings1up for a mutable borrowr3. - Rule 2 is the dangling-reference check:
valid_refis declared in the outer scope but assigned inside an inner scope to&s4. - When the inner
{ }ends,s4is dropped — ifvalid_refwere used afterward, it would point to freed heap memory (a dangling pointer / use-after-free). - The borrow checker rejects this at compile time with "
s4does not live long enough" — this is lifetime analysis, the foundation for Day 02.
Zero Memory Bugs at Runtime
No dangling pointers, no use-after-free, no double-free. The borrow checker catches them all at compile time — your production binary ships without a class of bugs.
No GC Pauses
No garbage collector means predictable, low-latency performance. Critical for real-time systems, game engines, trading systems, and high-throughput web servers.
Thread Safety Guaranteed
Ownership rules extend to concurrency. You cannot accidentally share mutable state across threads — the compiler prevents data races entirely.
Explicit Resource Management
RAII via Drop: file handles, DB connections, mutex locks — all released deterministically when the owner goes out of scope. No leaks by design.
String, Vec.Copy: implicitly duplicates stack-only data. The original stays valid. Types like
i32, bool, f64 implement Copy.Clone: explicit, deep copy of heap data via
.clone(). Both variables own independent data. Use it consciously — it has a runtime cost.
&mut T) at any point in time, and zero immutable references when a mutable reference is active. This prevents data races at compile time — if multiple references could mutate simultaneously, you'd have undefined behavior. Rust makes the concurrency contract explicit and statically verified.
&str vs String?
+
&str (a string slice / borrowed view) for read-only access to string data — function parameters, string literals, slices of a String. It's zero-copy and cheap.Use
String when you need to own the string data, grow it, or return it from a function. The general rule: prefer &str in function signatures for flexibility; use String when you need ownership or mutation.
Drop trait do, and how does it relate to ownership?
+
Drop trait defines custom cleanup logic that runs automatically when an owner goes out of scope — like a destructor. Rust calls drop() in reverse order of creation. This is RAII (Resource Acquisition Is Initialization): resources (file handles, locks, heap memory) are deterministically released when the owning struct is dropped. No GC, no explicit free needed.
| Pattern | What It Solves | Used In |
|---|---|---|
| Rc<T> / Arc<T> | Multiple ownership via reference counting. Arc is thread-safe. Use when you genuinely need shared ownership. |
Graph structures, shared caches, async runtimes |
| Cell<T> / RefCell<T> | Interior mutability — mutate data through an immutable reference. RefCell moves borrow checking to runtime. |
UI trees, mock objects, single-threaded shared state |
| Cow<'a, B> | Clone-on-Write — borrow data until mutation is needed, then clone. Avoids unnecessary allocations. | Parsers, serializers, string processing in hot paths |
| Pin<T> | Prevents a value from being moved in memory. Required for self-referential structs and async/await state machines. |
async futures, tokio tasks, custom allocators |
| ManuallyDrop<T> | Opt out of automatic drop(). Gives you manual control over destruction order. Used in unsafe code. |
FFI, custom allocators, arena memory management |
use std::cell::RefCell; use std::rc::Rc; fn main() { // Multiple owners + interior mutability without unsafe let shared = Rc::new(RefCell::new(vec![1, 2, 3])); let clone_a = Rc::clone(&shared); let clone_b = Rc::clone(&shared); clone_a.borrow_mut().push(4); // runtime borrow check clone_b.borrow_mut().push(5); println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5] // ⚠️ panics at runtime if you violate borrow rules // Use Arc> instead for multi-threaded scenarios }