Skip to content

Ownership and Borrowing in Rust: Memory Safety Without Garbage Collection

Introduction to Rust's Memory Management

Rust solves one of the most challenging problems in systems programming: finding the balance between speed, safety, concurrency, and portability. While languages like C and C++ are blazingly fast but unsafe, and languages like Python provide garbage collection but with performance overhead, Rust achieves memory safety without a garbage collector through its unique ownership system.

Why Ownership Exists

Traditional approaches to memory management have significant drawbacks:

  1. Manual Memory Management (C/C++): Developers control memory allocation and deallocation, leading to bugs like:
  2. Double-free errors
  3. Memory leaks
  4. Use-after-free vulnerabilities

  5. Garbage Collection (Java, Python): Automatic memory management that stops program execution to clean up memory, causing:

  6. Performance overhead
  7. Unpredictable pauses
  8. Not suitable for real-time systems

Rust's ownership system provides memory safety at compile time, eliminating these issues without runtime overhead.

The Three Rules of Ownership

Understanding ownership requires memorizing these fundamental rules:

  1. Each value in Rust has an owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value will be dropped

Rule 1: Each Value Has an Owner

fn main() {
    let s1 = String::from("rust");  // s1 is the owner of the string "rust"
}

Here, s1 owns the String value "rust". The ownership relationship is clear and unambiguous.

Rule 2: Only One Owner at a Time

fn main() {
    let s1 = String::from("rust");
    let s2 = s1;  // Ownership transfers from s1 to s2

    // println!("{}", s1);  // ❌ This would cause a compile error
    println!("{}", s2);     // ✅ This works - s2 is now the owner
}

When we assign s1 to s2, ownership is moved (not copied). s1 is no longer valid after this point.

Rule 3: Values are Dropped When Owner Goes Out of Scope

fn main() {
    {
        let s1 = String::from("rust");
        // s1 is valid here
    } // s1 goes out of scope and is dropped here

    // s1 is no longer accessible
}

Understanding References and Borrowing

Borrowing allows you to use values without taking ownership. This is accomplished through references.

Creating References

fn main() {
    let s1 = String::from("rust");
    let len = calculate_length(&s1);  // Pass a reference, not the value

    println!("The length of '{}' is {}", s1, len);  // s1 is still valid!
}

fn calculate_length(s: &String) -> usize {  // s is a reference to a String
    s.len()  // We can read the data without owning it
}

The & symbol creates a reference. The function calculate_length borrows the string rather than taking ownership.

Immutable vs Mutable References

By default, references are immutable:

fn main() {
    let mut s = String::from("hello");

    // Immutable reference
    let r1 = &s;
    println!("{}", r1);

    // Mutable reference (requires mut keyword for both variable and reference)
    let r2 = &mut s;
    r2.push_str(" world");
    println!("{}", r2);
}

The Borrowing Rules

Rust enforces these rules at compile time:

  1. You can have either one mutable reference OR any number of immutable references
  2. References must always be valid
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;      // ✅ Immutable reference
    let r2 = &s;      // ✅ Another immutable reference
    // let r3 = &mut s;  // ❌ Cannot have mutable reference while immutable refs exist

    println!("{} {}", r1, r2);

    let r3 = &mut s;  // ✅ Now this works - immutable references are no longer used
    r3.push_str(" world");
}

Practical Example: Bank Account

Here's a practical example showing how borrowing prevents data races:

struct BankAccount {
    owner: String,
    balance: f64,
}

impl BankAccount {
    fn withdraw(&mut self, amount: f64) {
        println!("Withdrawing {} from account owned by {}", amount, self.owner);
        self.balance -= amount;
    }

    fn check_balance(&self) {
        println!("Account owned by {} has a balance of {}", self.owner, self.balance);
    }
}

fn main() {
    let mut account = BankAccount {
        owner: "Alice".to_string(),
        balance: 150.55,
    };

    // Immutable borrow to check balance
    account.check_balance();

    // Mutable borrow to withdraw money
    account.withdraw(45.50);

    // Check balance again
    account.check_balance();
}

Each borrow occurs in its own scope, preventing simultaneous mutable and immutable access.

Memory Safety Benefits

Rust's ownership system prevents common memory safety issues:

Dangling Pointers

// This won't compile - Rust prevents dangling references
fn dangle() -> &String {  // ❌ Missing lifetime specifier
    let s = String::from("hello");
    &s  // s is dropped when function ends, making this reference invalid
}

Data Races

The borrowing rules prevent data races at compile time by ensuring that mutable access is exclusive.

Use After Free

Once a value is moved, the original variable becomes invalid, preventing use-after-free bugs.

Key Takeaways

  1. Ownership provides memory safety without garbage collection
  2. Moving transfers ownership; borrowing provides temporary access
  3. References allow sharing data without compromising safety
  4. Compile-time checks prevent entire classes of memory bugs
  5. The borrowing checker enforces these rules automatically

This system makes Rust uniquely positioned for systems programming where both performance and safety are critical. The compiler catches memory safety violations before your code ever runs, eliminating a major source of security vulnerabilities and crashes in systems software.

Understanding ownership and borrowing is fundamental to writing effective Rust code. While it may seem restrictive at first, these constraints guide you toward writing safer, more concurrent code that performs excellently in production environments.