No history yet

Ownership and Memory

Memory Without a Garbage Collector

Most programming languages manage memory in one of two ways. Some use a garbage collector (GC) that periodically scans for unused memory and cleans it up. Others require the programmer to manually allocate and free memory. Both approaches have trade-offs. Garbage collection adds runtime overhead, while manual management can lead to bugs like memory leaks or dangling pointers.

Rust takes a third path. It manages memory through a system of ownership with a set of rules that the compiler checks at compile time. This system provides the memory safety of a garbage collector without the performance cost. To understand ownership, we first need to look at how a program stores data in memory.

The Stack and the Heap

All computer programs use two main areas of memory to store data: the stack and the heap. They have different structures and purposes.

The stack is highly organized and fast. Data is added in a last-in, first-out (LIFO) order, like a stack of plates. You can only add or remove items from the top. All data stored on the stack must have a known, fixed size. This makes it incredibly efficient for the processor to access.

The heap is less organized. When you need to store data but don't know its exact size at compile time, or if you need it to live longer than a specific function, you can store it on the heap. The operating system finds an empty spot in memory, marks it as in use, and returns a pointer—the address of that location. Accessing data on the heap is slower than the stack because it involves following that pointer to find the data.

Managing heap data is what ownership is all about. Keeping track of what parts of the code are using which data on the heap, minimizing duplicate data, and cleaning up unused data are the problems that Rust's ownership system solves.

The Rules of Ownership

The ownership system is governed by a few simple rules, which are checked by the compiler. If any of these rules are violated, the program won't compile.

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped.

Let's break this down with an example. A scope is the range within a program for which an item is valid. For a variable, this is typically from where it is declared to the end of the current block (curly braces).

{
    // s is not valid here, it’s not yet declared
    let s = String::from("hello"); // s is valid from this point forward

    // do stuff with s
    println!("{}", s);

} // this scope is now over, and s is no longer valid

When the variable s comes into scope, it requests memory from the heap for the string data. When it goes out of scope at the closing brace }, Rust automatically calls a special function called drop. This function returns the memory that s was using back to the operating system. This happens deterministically at the end of the scope, ensuring memory is freed as soon as it's no longer needed.

Moving and Cloning

Things get more interesting when we assign one variable to another. For simple, fixed-size types stored entirely on the stack, the behavior is what you'd expect.

let x = 5;
let y = x; // A copy of the value 5 is made

println!("x = {}, y = {}", x, y); // Both x and y are valid

Since integers have a known size, both x and y are pushed onto the stack as two separate values. But what about a String, which stores its data on the heap?

let s1 = String::from("hello");
let s2 = s1;

// println!("s1 is {}", s1); // This line would cause a compile error!

This looks similar, but it works differently. A String is made of three parts stored on the stack: a pointer to the actual data on the heap, a length, and a capacity. When we assign s1 to s2, Rust copies this stack data. It does not copy the heap data. If it did, both s1 and s2 would point to the same memory location. When they both went out of scope, they would both try to free the same memory—a bug called a double free.

To ensure memory safety, Rust considers s1 to no longer be valid after the assignment. We say that ownership of the string has been moved from s1 to s2.

If you truly need a full, independent copy of heap data, you can use the clone() method. This will duplicate the data on the heap, which can be a more expensive operation.

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2); // Both are valid

This system of ownership, moving, and dropping is the foundation of Rust's memory safety guarantees. It prevents a whole class of common bugs at compile time, allowing you to write fast, safe code without a garbage collector. In the next sections, we'll explore how Rust allows you to use data without taking ownership through a concept called borrowing.

Quiz Questions 1/5

What is a key advantage of Rust's ownership system compared to a traditional garbage collector (GC)?

Quiz Questions 2/5

In a Rust program, where is data with a size that is unknown at compile time, like a String, typically stored?