Ownership and Borrowing in Rust
Introduction
- Rust's most distinctive feature: ownership system
- Enables memory safety without garbage collection
- Compile-time guarantees with zero runtime cost
- Three key concepts: ownership, borrowing, and lifetimes
Prework
Prework Readings
Read the following sections from "The Rust Programming Language" book:
- Chapter 4: Understanding Ownership - All sections
Pre-lecture Reflections
Before class, consider these questions:
- What problems does Rust's ownership system solve compared to manual memory management?
- How does ownership differ from garbage collection in other languages?
- What is the difference between moving and borrowing a value?
- When would you use
Box<T>instead of storing data on the stack? - How do mutable and immutable references help prevent data races?
Memory Layout: Stack vs Heap
Stack:
- Fast, fixed-size allocation
- LIFO (Last In, First Out) structure
- Stores data with known, fixed size at compile time
- Examples: integers, booleans, fixed-size arrays
Heap:
- Slower, dynamic allocation
- For data with unknown or variable size
- Allocator finds space and returns a pointer
- Examples: String, Vec, Box
Stack Memory Example
fn main() { let x = 5; // stored on stack let y = true; // stored on stack let z = x; // copy of value on stack println!("{}, {}", x, z); // both still valid }
- Simple types implement
Copytrait - Assignment creates a copy, both variables remain valid
String and the Heap
Heap Memory: The String Type
Let's look more closely at the String type.
#![allow(unused)] fn main() { let s1 = String::from("hello"); }
Stringstores pointer, length, capacity on stack- Actual string data stored on heap

In fact we can inspect the memory layout of a String:
#![allow(unused)] fn main() { let mut s = String::from("hello"); println!("&s:{:p}", &s); println!("ptr: {:p}", s.as_ptr()); println!("len: {}", s.len()); println!("capacity: {}\n", s.capacity()); // Let's add some more text to the string s.push_str(", world!"); println!("&s:{:p}", &s); println!("ptr: {:p}", s.as_ptr()); println!("len: {}", s.len()); println!("capacity: {}", s.capacity()); }
Shallow Copy with Move
fn main() { let s1 = String::from("hello"); // s1 has three parts on stack: // - pointer to heap data // - length: 5 // - capacity: 5 let s2 = s1; // shallow copy of stack data println!("{}", s1); // ERROR! s1 is no longer valid println!("{}", s2); // OK }
Stringstores pointer, length, capacity on stack- Actual string data stored on heap
Shallow Copy:
- Copying the pointer, length, and capacity
- The actual string data is not copied
- The owner of the string data is transferred to the new structure

#![allow(unused)] fn main() { let s1 = String::from("hello"); println!("&s1:{:p}", &s1); println!("ptr: {:p}", s1.as_ptr()); println!("len: {}", s1.len()); println!("capacity: {}\n", s1.capacity()); let s2 = s1; println!("&s2:{:p}", &s2); println!("ptr: {:p}", s2.as_ptr()); println!("len: {}", s2.len()); println!("capacity: {}", s2.capacity()); }
The Ownership Rules
- Each value in Rust has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
These rules prevent:
- Double free errors
- Use after free
- Data races
Ownership Transfer: Move Semantics
fn main() { let s1 = String::from("hello"); let s2 = s1; // ownership moves from s1 to s2 // s1 is now invalid - compile error if used println!("{}", s2); // OK // When s2 goes out of scope, memory is freed }
- Move prevents double-free
- Only one owner can free the memory
Clone: Deep Copy
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); // deep copy of heap data println!("s1 = {}, s2 = {}", s1, s2); // both valid }
clone()creates a full copy of heap data- Both variables are independent owners
- More expensive operation
Vec and the Heap
Vec: Dynamic Arrays on the Heap
What is Vec?
Vec<T>is Rust's growable, heap-allocated array type- Generic over type
T(e.g.,Vec<i32>,Vec<String>) - Contiguous memory allocation for cache efficiency
- Automatically manages capacity and growth
Three ways to create a Vec:
#![allow(unused)] fn main() { // 1. Empty vector with type annotation let v1: Vec<i32> = Vec::new(); // 2. Using vec! macro with initial values let v2 = vec![1, 2, 3, 4, 5]; // 3. With pre-allocated capacity let v3: Vec<i32> = Vec::with_capacity(10); }
Vec Memory Structure
#![allow(unused)] fn main() { let mut v = Vec::new(); v.push(1); v.push(2); v.push(3); // Vec structure (on stack): // - pointer to heap buffer // - length: 3 (number of elements) // - capacity: (at least 3, often more) println!("&v:{:p}", &v); println!("ptr: {:p}", v.as_ptr()); println!("Length: {}", v.len()); println!("Capacity: {}", v.capacity()); }
- Pointer: points to heap-allocated buffer
- Length: number of initialized elements
- Capacity: total space available before reallocation
Vec Growth and Reallocation
fn main() { let mut v = Vec::new(); println!("Initial capacity: {}", v.capacity()); // 0 v.push(1); println!("After 1 push: {}", v.capacity()); // typically 4 v.push(2); v.push(3); v.push(4); v.push(5); // triggers reallocation println!("After 5 pushes: {}", v.capacity()); // typically 8 }
- Capacity doubles when full (amortized O(1) push)
- Reallocation: new buffer allocated, old data copied
- Pre-allocate with
with_capacity()to avoid reallocations
Accessing Vec Elements
fn main() { let v = vec![10, 20, 30, 40, 50]; // Indexing - panics if out of bounds // Try with index 5 and see what happens let third = v[2]; println!("Third element: {}", third); // Using get() - returns Option<T> // Safely handles out of bounds indices match v.get(2) { Some(value) => println!("Third element: {}", value), None => println!("No element at index 2"), } }
Option<T>
Option<T> is an enum that can be either Some(T) or None.
Defined in the standard library as:
enum Option<T> {
Some(T),
None,
}
Let's you handle the case where there is no return value.
fn main() { let v = vec![1, 2, 3, 4, 5]; // Try with index 5 and see what happens match v.get(0) { Some(value) => println!("Element: {}", value), None => println!("No element at index"), } }
Modifying Vec Elements
fn main() { let mut v = vec![1, 2, 3, 4, 5]; // Direct indexing for modification v[0] = 10; // Adding elements v.push(6); // add to end // Removing elements let last = v.pop(); // remove from end, returns Option<T> // Insert/remove at position v.insert(2, 99); // insert 99 at index 2 v.remove(1); // remove element at index 1 println!("{:?}", v); }
Note that
insertandremovecan be expensive operations and are time. On the other hand,pushandpopare time.
Vec Ownership
fn main() { let v1 = vec![1, 2, 3, 4, 5]; let v2 = v1; // ownership moves // println!("{:?}", v1); // ERROR! println!("{:?}", v2); // OK let v3 = v2.clone(); // deep copy println!("{:?}, {:?}", v2, v3); // both OK }
- Vec follows same ownership rules as String
- Move transfers ownership of heap allocation
Functions and Ownership
Functions and Ownership
fn takes_ownership(s: String) { println!("{}", s); } // s is dropped here fn main() { let s = String::from("hello"); takes_ownership(s); // println!("{}", s); // ERROR! s was moved }
- Passing to function transfers ownership
- Original variable becomes invalid
Returning Ownership
fn gives_ownership(s: String) -> String { let new_s = s + " world"; new_s // ownership moves to caller } fn main() { let s1 = String::from("hello"); let s2 = gives_ownership(s1); println!("{}", s2); // OK }
- Return value transfers ownership out of function
- Caller becomes new owner
References: Borrowing Without Ownership
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // borrow with & println!("'{}' has length {}", s1, len); // s1 still valid! } fn calculate_length(s: &String) -> usize { s.len() } // s goes out of scope, but doesn't own data
&creates a reference (borrow)- Original owner retains ownership
- Reference allows reading data
Immutable References
fn main() { let s = String::from("hello"); let r1 = &s; // immutable reference let r2 = &s; // another immutable reference let r3 = &s; // yet another println!("{}, {}, {}", r1, r2, r3); // all valid // Let's take a look at the memory layout println!("&s: {:p}, s.as_ptr(): {:p}", &s, s.as_ptr()); println!("&r1: {:p}, r1.as_ptr(): {:p}", &r1, r1.as_ptr()); println!("&r2: {:p}, r2.as_ptr(): {:p}", &r2, r2.as_ptr()); println!("&r3: {:p}, r3.as_ptr(): {:p}", &r3, r3.as_ptr()); }
- Multiple immutable references allowed simultaneously
- Cannot modify through immutable reference
// ERROR fn main() { let s = String::from("hello"); change(&s); println!("{}", s); } fn change(s: &String) { s.push_str(", world"); }
Mutable References
fn main() { let mut s = String::from("hello"); change(&mut s); // mutable reference with &mut println!("{}", s); // prints "hello, world" } fn change(s: &mut String) { s.push_str(", world"); }
&mutcreates mutable reference- Allows modification of borrowed data
Mutable Reference Restrictions
fn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; // ERROR! Only one mutable reference println!("{}", r1); }
- Only ONE mutable reference at a time
- Prevents data races at compile time
- No simultaneous readers when there's a writer
Mixing References: Not Allowed
fn main() { let mut s = String::from("hello"); let r1 = &s; // immutable let r2 = &s; // immutable let r3 = &mut s; // ERROR! Can't have mutable with immutable println!("{}, {}", r1, r2); }
- Cannot have mutable reference while immutable references exist
- Immutable references expect data won't change
Reference Scopes and Non-Lexical Lifetimes
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{}, {}", r1, r2); // r1 and r2 no longer used after this point let r3 = &mut s; // OK! Previous references out of reference scope println!("{}", r3); }
- Reference scope: from introduction to last use, rather than lexical scope (till end of block)
- Non-lexical lifetimes allow more flexible borrowing
Vec with References
fn main() { let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; // immutable borrow // v.push(6); // ERROR! Can't mutate while borrowed println!("First element: {}", first); v.push(6); // OK now, first is out of reference scope }
- Borrowing elements prevents mutation of Vec
- Protects against invalidation (reallocation)
Function Calls: Move vs Reference vs Mutable Reference
fn process_string(s: String) { } // takes ownership (move) fn read_string(s: &String) { } // immutable borrow fn modify_string(s: &mut String) { } // mutable borrow fn main() { let mut s = String::from("hello"); read_string(&s); // borrow modify_string(&mut s); // mutable borrow read_string(&s); // borrow again process_string(s); // move // s is now invalid }
This is ok because the move (ownership transfer) happens last, so the other references are still valid.
Key Takeaways
- Stack: fixed-size, fast; Heap: dynamic, flexible
- Ownership ensures memory safety without garbage collection
- Move semantics prevent double-free
- Borrowing allows temporary access without ownership transfer
- One mutable reference XOR many immutable references
- References must be valid (no dangling pointers)
- Compiler enforces these rules at compile time
Best Practices
- Prefer borrowing over ownership transfer when possible
- Use immutable references by default
- Keep mutable reference scope minimal
- Let the compiler guide you with error messages
- Clone only when necessary (performance cost)
- Understand whether functions need ownership or just access
In-Class Exercise
Challenge: Fix the Broken Code
The following code has several ownership and borrowing errors. Your task is to fix them so the code compiles and runs correctly.
fn main() { let mut numbers = vec![1, 2, 3, 4, 5]; // Task 1: Calculate sum without taking ownership let total = calculate_sum(numbers); // Task 2: Double each number in the vector double_values(numbers); // Task 3: Print both the original and doubled values println!("Original sum: {}", total); println!("Doubled values: {:?}", numbers); // Task 4: Add new numbers to the vector add_numbers(numbers, vec![6, 7, 8]); println!("After adding: {:?}", numbers); } fn calculate_sum(v: Vec<i32>) -> i32 { let mut sum = 0; for num in v { sum += num; } sum } fn double_values(v: Vec<i32>) { for num in v { num *= 2; } } fn add_numbers(v: Vec<i32>, new_nums: Vec<i32>) { for num in new_nums { v.push(num); } }
Hints:
- Think about which functions need ownership vs borrowing
- Consider when you need
&vs&mut - Remember: you can't modify through an immutable reference
- The original vector should still be usable in
mainafter function calls