Understand the memory problems Rust solves and why stack vs heap matters.
Before we dive into ownership, we need to understand the problem Rust is solving. This lesson is conceptual. No code challenges. Just ideas that will make everything click.
By the end of this lesson, you'll understand why memory bugs are dangerous, how other languages handle them, and why the stack vs heap distinction is the foundation for understanding Rust's ownership system.
In languages like C and C++, you manually manage memory. You allocate it. You free it. And you hope you got it right.
When you don't get it right, bad things happen:
Double free: You free the same memory twice. The second free corrupts the allocator's internal state. Your program crashes. Or worse, it keeps running with corrupted data.
Use after free: You free memory, then keep using the pointer. That memory might now belong to something else. You're reading or writing someone else's data.
Dangling pointers: You have a pointer to memory that no longer exists. Every time you dereference it, you're playing Russian roulette.
Memory bugs aren't just crashes. They're the root cause of countless security exploits. Buffer overflows, use-after-free vulnerabilities, and memory corruption attacks have been responsible for some of the most severe security breaches in computing history.
These bugs are insidious. They don't always crash immediately. Sometimes they corrupt data silently. Sometimes they only fail under specific conditions that are nearly impossible to reproduce in testing.
Most modern languages took the obvious path: take memory management away from the programmer with garbage collection.
Rust takes a different approach. It enables memory safe guarantees without needing a garbage collector. Instead of tracking memory at runtime, Rust enforces rules at compile time. When your code compiles, these memory bugs are already impossible.
Rust asked: What if we could prevent memory bugs at compile time instead of cleaning them up at runtime? No garbage collector. Just rules that the compiler enforces before your code ever runs.
To understand Rust's solution, you need to understand where data lives.
The stack is a contiguous block of memory managed by a single pointer: the stack pointer. It works like a tower of plates. You add plates to the top. You remove plates from the top. Always in order.
When you call a function, a new "stack frame" gets pushed. This frame contains the function's local variables, parameters, and return address. When the function returns, the frame gets popped. Gone. Instantly.
Allocating on the stack is one CPU instruction. Move the stack pointer. That's it. No searching for free space. No bookkeeping. Just move a pointer.
"Freeing" stack memory? There's nothing to free. When a function returns, you just move the pointer back. The memory is reclaimed instantly.
i32, u64, usizef32, f64boolchar[i32; 5](i32, bool, char)The key constraint: the compiler must know the exact size at compile time. An i32 is always 4 bytes. A [u8; 100] is always 100 bytes. The compiler can plan exactly where everything goes.
The heap is the wild west. A large, unstructured pool of memory.
When you need heap memory, you ask an allocator. The allocator searches for a free chunk, marks it as used, and returns a pointer. The data can live anywhere. No ordering. Scattered across the heap.
Freeing heap memory means telling the allocator "I'm done with this chunk." The allocator marks it as available for future allocations.
Over time, the heap fragments. Freed chunks scatter between used ones. Allocating gets slower as the allocator searches for suitably-sized gaps.
Every heap allocation requires work. Finding space. Updating bookkeeping. Following pointers to scattered data hurts CPU cache performance. The heap is powerful, but it's not free.
What lives on the heap:
String - can grow and shrinkVec<T> - dynamic arraysHashMap<K, V> - dynamic key-value storageBox<T> - explicitly heap-allocated valuesHere's the mental model that makes this click:
The stack is like a tower of plates. You add plates to the top and remove them from the top. Simple. Fast. Ordered.
The heap is like a parking lot. Cars come and go from random spots. You need to search for an open space. You need to remember where you parked. Cars leave gaps that aren't always the right size for the next car.
The notepad is efficient but inflexible. You can only add or remove from one end. Everything must be the same "width."
The parking lot handles any size car at any time, but finding spots and avoiding collisions requires constant management.
Here's where it gets interesting. When you create a Vec<i32> in Rust, it lives in both places.
On the stack: a small struct containing three things:
This struct is always exactly 24 bytes on a 64-bit system. Three 8-byte values. Fixed size. Known at compile time. Lives on the stack.
On the heap: the actual data. Zero bytes if the vector is empty. 40 megabytes if you've got 10 million integers. Dynamic. Grows and shrinks as needed.
This dual nature is elegant. The stack portion is fixed-size, so it can live on the stack. The heap portion can be any size, so it lives on the heap. The stack portion points to the heap portion. Best of both worlds.
When the Vec goes out of scope, Rust automatically drops the stack portion AND frees the heap portion. No manual cleanup. No garbage collector. Just scope-based deterministic cleanup.
| Aspect | Stack | Heap |
|---|---|---|
| Size | Fixed at compile time | Dynamic, can change |
| Speed | Blazingly fast (one instruction) | Slower (search, bookkeeping) |
| Allocation | Just move a pointer | Find free space, track it |
| Deallocation | Just move a pointer back | Mark as free, update tracking |
| Fragmentation | None | Accumulates over time |
| Data access | Contiguous, cache-friendly | Scattered, cache misses |
| Lifetime | Tied to function scope | Until explicitly freed |
This is why you don't copy 10 million elements when you pass a vector to a function.
If Rust copied the entire heap data, function calls would be catastrophically slow. Pass a 10 million element vector? Copy 40 megabytes. That's absurd.
Instead, Rust moves the 24-byte stack struct. The heap data stays exactly where it is. The new owner just gets the pointer to it.
A move in Rust is copying a few bytes on the stack. The potentially massive heap data doesn't budge. This is why Rust can be both safe and fast.
Now you can see why heap data needs special handling.
Stack data doesn't need ownership tracking. It evaporates when its scope ends. The stack pointer moves back, and the memory is instantly reclaimed. No coordination needed.
Heap data needs someone responsible for freeing it. If nobody frees it, you leak memory. If two things try to free it, you double-free. If you use it after freeing, you use-after-free.
Rust's answer is elegant:
This is the ownership system. Compile-time rules that prevent memory bugs without runtime overhead.
A move is Rust's elegant middle ground: the efficiency of not copying massive data, the safety of single ownership, automatic cleanup when scope ends. All enforced at compile time with zero runtime cost.
You now understand the why behind Rust's ownership system:
Now that you understand WHY memory management matters and what stack and heap are, let's see Rust's elegant solution in action. In the next lesson, we'll learn the three ownership rules that make all of this work.