Why this matters
Ownership, the idea at the center of Rust, is a set of rules about memory: who is responsible for freeing a piece of memory, and when. Those rules only make sense once you can picture where values actually live. Languages like Python and Java hide this behind a garbage collector; C exposes it and leaves you to get it right by hand. Rust exposes it and has the compiler check that you got it right. This lesson builds the mental picture; the next four use it constantly.
A process's memory
When your program runs, the operating system gives it a range of addresses. Simplified:
high addresses
+---------------------------+
| stack | grows downward: one frame per active function call
| | |
| v |
| |
| ^ |
| | |
| heap | grows as the program asks for memory
+---------------------------+
| static data | string literals, constants, global variables
+---------------------------+
| code | the machine instructions
+---------------------------+
low addresses
- Code and static data are loaded from the executable and stay for the whole run. A string literal like
"hello"lives here, which is why it is always available. - The stack holds function call frames.
- The heap holds memory requested while the program runs.
The stack
You met stack frames in module 2. Each function call pushes a frame with space for its parameters and local variables; returning pops it. The CPU keeps a stack pointer register pointing at the top, so creating a frame is just subtracting a number from that register. Nothing could be faster.
fn main() { let a = 1; let b = add(a, 2); } while add runs:
fn add(x: i32, y: i32) -> i32 { x + y }
+------------------+
| add x = 1 | <- stack pointer
| y = 2 |
+------------------+
| main a = 1 |
| b = ? |
+------------------+
The stack has two strict properties:
- Every frame's layout is fixed at compile time. The compiler must know the size of every local variable to compute offsets like "b is 4 bytes above the frame start". So stack values must have a size known at compile time: integers, floats,
char,bool, tuples and arrays of those. - A frame dies when its function returns. Its memory is immediately reused by the next call. Anything that must outlive the call cannot live in the frame.
It is also small: 8 MB for the main thread on a typical Linux system. A local array of a hundred million bytes crashes with a "stack overflow".
The heap
The heap is a large pool of memory managed by an allocator (part of the standard library, on top of the operating system). Your program can ask it, at run time, for a block of any size:
- Allocate: "give me 11 bytes". The allocator finds a free block and returns its address.
- Use the block through that address, a pointer.
- Free (deallocate): "I am done with the block at this address". It becomes available again.
The heap solves both stack limitations: sizes can be decided at run time, and a block lives until it is freed, regardless of which function calls come and go. The costs: allocation takes real work (finding a suitable free block), and access goes through a pointer.
A String: both at once
String is Rust's growable, heap-allocated text type, and it is the perfect example:
fn main() {
let s = String::from("hello");
println!("{s}: len {}, capacity {}", s.len(), s.capacity());
println!("String value itself: {} bytes", std::mem::size_of::<String>());
println!("heap buffer at {:p}", s.as_ptr());
}
hello: len 5, capacity 5
String value itself: 24 bytes
heap buffer at 0x55fbeccd5b10
(The address will differ on every run; {:p} prints a pointer.) In memory:
stack (main's frame) heap
+----------------------+ +---+---+---+---+---+
s | ptr --------------------> | h | e | l | l | o |
| len 5 | +---+---+---+---+---+
| capacity 5 |
+----------------------+
24 bytes, fixed size 5 bytes, size decided at run time
The String value on the stack is three machine words (8 bytes each on 64-bit): a pointer to the heap buffer, the length (bytes in use) and the capacity (bytes allocated). Its size is fixed, so it fits in a stack frame; the text itself, whose size is not known at compile time, is on the heap.
When you append beyond the capacity, the String allocates a bigger buffer (usually double), copies the bytes over, frees the old buffer and updates its pointer:
fn main() {
let mut s = String::new();
println!("len {:2} cap {:2}", s.len(), s.capacity());
for word in ["Rust", " is", " a", " systems", " language"] {
s.push_str(word);
println!("len {:2} cap {:2} {s:?}", s.len(), s.capacity());
}
}
len 0 cap 0
len 4 cap 8 "Rust"
len 7 cap 8 "Rust is"
len 9 cap 16 "Rust is a"
len 17 cap 32 "Rust is a systems"
len 26 cap 32 "Rust is a systems language"
An empty String allocates nothing (capacity 0). Doubling means that appending many times costs only a few reallocations overall.
The central question: who frees the heap block?
Stack memory takes care of itself: frames vanish on return. Heap memory does not. Something must free every block exactly once, after its last use. Get this wrong and you have one of the classic bugs:
| bug | what happens |
|---|---|
| memory leak | a block is never freed; the program's memory grows until it is killed |
| double free | a block is freed twice; the allocator's bookkeeping is corrupted, often exploitable |
| use after free | a block is used after being freed; you read or overwrite someone else's data |
| dangling pointer | a pointer to memory that is no longer valid, waiting to cause one of the above |
Languages answer the question in three ways:
- Manual (C): you call
freeyourself. Fast and flexible; the bugs above are common, and they are a large share of real security vulnerabilities. - Garbage collection (Python, Java, Go): a runtime system finds unreachable blocks and frees them periodically. Safe and easy; costs memory and CPU time, and you do not control when cleanup happens.
- Ownership (Rust): every heap block has exactly one owner variable, and the block is freed automatically, at a point known at compile time, when the owner goes out of scope. The compiler checks the rules, so leaks of this kind, double frees and use-after-free do not happen in safe Rust, and there is no garbage collector.
The rest of this module explains how ownership works. The important part to take from this lesson is the picture: a small fixed-size handle on the stack, owning a buffer on the heap. String is one such type; Vec (a growable list) and Box (a single heap value) are others.
Common mistakes
Thinking size_of::<String>() depends on the text. It is always 24 bytes on a 64-bit machine; it measures the handle, not the heap buffer.
Thinking string literals are on the heap. "hello" is in the executable's static data. A literal has type &str, a pointer and a length pointing into that static data. String::from("hello") copies it to the heap.
Huge arrays as locals. let big = [0u8; 100_000_000]; overflows the stack:
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Large or variable-size data belongs on the heap, in a Vec (module 9) or Box (module 13).
Believing the heap is slow. Allocation costs something; access does not, beyond following one pointer. Allocating inside a tight loop is the thing to avoid, not the heap in general.
Recap
- The stack holds function frames: fixed-size values, allocated and freed automatically with calls.
- The heap holds memory requested at run time, of any size, living until it is freed.
- A
Stringis a 24-byte handle (pointer, length, capacity) on the stack plus a heap buffer. - Growing past capacity reallocates: new buffer, copy, free the old one.
- Every heap block must be freed exactly once after its last use; Rust's answer is ownership, checked at compile time.
Going further: run the growth example with push of single characters in a loop of 100 and print the capacity only when it changes. Then read the first section of the Rust Book's chapter 4 ("What Is Ownership?") for a second explanation of the same picture.