Step 1 of 6
Stack and heap: where values live
Every value your program works with is stored somewhere in the computer's memory. To understand Rust's most famous feature, ownership, you first need a picture of the two places a running program keeps its data: the stack and the heap.
The stack
The stack holds the local variables of the functions that are running right now. Each function call gets its own block of space, a stack frame, big enough for all of its variables. When the function returns, its whole frame is thrown away in one go. Making and removing frames is just moving a pointer up and down, so the stack is extremely fast.
The catch: everything on the stack must have a size known at compile time. An i32 is always 4 bytes, an f64 8 bytes, an array [i32; 3] 12 bytes. The compiler lays out each frame before the program ever runs.
The heap
What about text the user types in, or a list that grows while the program runs? Their size isn't known until run time, and it can change. That data goes on the heap: a big pool of memory where the program can ask for a block of any size while it runs. Asking for a block is called allocating. The allocator finds free space and hands back its address, a pointer. When the program is done with the block, it must give it back (free it) so the space can be reused.
A String uses both. The text lives on the heap. The String value itself is a small, fixed-size handle on the stack holding three numbers: a pointer to the text, its length (bytes in use) and its capacity (bytes reserved).
fn main() {
let lives = 3;
let name = String::from("Ferris");
}
STACK (main's frame) HEAP
┌───────┬──────────────────┐
│ lives │ 3 │
├───────┼──────────────────┤ ┌───┬───┬───┬───┬───┬───┐
│ name │ ptr ────────────┼────────► │ F │ e │ r │ r │ i │ s │
│ │ len 6 │ └───┴───┴───┴───┴───┴───┘
│ │ capacity 6 │
└───────┴──────────────────┘
A Vec is built the same way: pointer, length and capacity on the stack, the elements on the heap. That's why both can grow: when they run out of capacity, they allocate a bigger block on the heap and move their contents there, while the handle on the stack keeps the same size.
You can check sizes with std::mem::size_of::<T>(), which tells you how many bytes a value of type T takes on the stack. The ::<T> part names the type you're asking about. Since Rust 1.80, size_of is one of the few functions available everywhere without its full path, so you can also write just size_of::<T>(), as this example does.
fn main() {
let short = String::from("hi");
let long = String::from("a much longer piece of text");
println!("i32: {} bytes", size_of::<i32>());
println!("[i32; 3]: {} bytes", size_of::<[i32; 3]>());
println!("String handle: {} bytes", size_of::<String>());
println!("text on the heap: {} and {} bytes", short.len(), long.len());
}
i32: 4 bytes
[i32; 3]: 12 bytes
String handle: 24 bytes
text on the heap: 2 and 27 bytes
The handle is 24 bytes (three 8-byte numbers on a 64-bit machine) whether the text is 2 bytes or 2 gigabytes. The text itself is somewhere else.
Who cleans up the heap?
Stack memory cleans itself up when a function returns. Heap memory doesn't: something has to free each block, exactly once, after the last time it's used. Programming languages answer this in three ways:
| Approach | Languages | The catch |
|---|---|---|
| You free memory by hand | C, C++ (malloc/free, new/delete) |
Forget it: a memory leak. Free twice: a double free. Use the memory after freeing it: a use-after-free. These bugs crash programs and are the classic source of security holes. |
| A garbage collector finds unused memory while the program runs | Java, Python, Go, JavaScript, C# | Safe, but it costs CPU time and memory, and can pause the program at moments you don't control. |
| Ownership: the compiler works out where to free, using rules it checks at compile time | Rust | You have to follow the rules. That's what this module teaches. |
Microsoft and Google's Chrome team have both reported that around 70% of their serious security bugs are memory-safety bugs like these. Rust's ownership rules make use-after-free and double free impossible in normal (safe) Rust, with no garbage collector and no run-time cost: the compiler inserts the code that frees memory exactly where it belongs.
Common mistakes
- Thinking a
Stringholds its text inline. It holds a pointer to the text. Copying the handle doesn't copy the text, which is exactly why the next steps matter. - Thinking string literals live on the heap.
"hi"is stored in the program's binary, read-only.String::from("hi")copies those bytes into a new heap block that theStringis responsible for. - Mixing up length and capacity.
.len()is how many bytes are in use. The capacity is how many are reserved, so that pushing more text doesn't have to allocate every time.
Your task
Fill in the blanks so the program prints the size of the String handle (the type name goes inside ::<>) and the number of bytes the city's text takes on the heap. It should print:
handle: 24 bytes
city text: 4 bytes
motto text: 17 bytes
Challenge 2
A Vec is a handle too. Fill in the type on the Vec handle line (a vector of i32) and the method that counts the vector's elements, so the program prints:
array: 16 bytes
Vec handle: 24 bytes
8 readings, still a 24-byte handle
Notice the difference: the array's numbers are all on the stack (4 × 4 bytes), while the Vec's numbers are on the heap.
Challenge 3
A String can grow because its text is on the heap. Starting from the empty String in the starter code (String::new() makes a String with no text yet), write a for loop that runs 4 times. Each time, push "==" onto bar with push_str and print bar. After the loop, print its length. The output should be:
==
====
======
========
bar: 8 bytes