Memory Management in Rust â Stack, Heap & RAII
In this tutorial, you'll learn about Memory Management in Rust. We cover key concepts, practical examples, and best practices.
Rust's memory management is built on the stack and heap with the RAII (Resource Acquisition Is Initialization) pattern, ensuring every resource is freed exactly once, deterministically, without a garbage collector.
What You'll Learn
In this tutorial, you'll learn how Rust manages memory on the stack and heap, how the RAII pattern works, and how resource cleanup is guaranteed at compile time.
Why It Matters
Memory bugs â leaks, double frees, use-after-free â account for roughly 70% of critical CVEs in C and C++ programs. Rust's memory management model eliminates these bugs entirely. Durga Antivirus Pro relies on this guarantee for its real-time file scanning engine where a single memory corruption could compromise the entire system.
Real-World Use
Database engines, browser rendering pipelines, game engines, and embedded firmware all manage memory intensely. The Rust compiler prevents memory errors while maintaining performance comparable to hand-optimized C++.
flowchart LR
subgraph Stack
F1[Frame: main]
F2[Frame: foo]
F3[Frame: bar]
end
subgraph Heap
B1[Box]
V1[Vec]
S1[String]
end
F2 -->|owns| B1
F3 -->|owns| V1
F1 -->|owns| S1
style Stack fill:#e6f3ff
style Heap fill:#fff3e6
Prerequisites: Rust basics. Understanding of the Rust systems programming overview helps.
Stack vs Heap
The stack is fast and deterministic. Each function call creates a frame that is popped when the function returns. The heap is more flexible but requires explicit allocation and deallocation.
fn main() {
// Stack-allocated: size known at compile time
let x: i32 = 42;
let y: f64 = 3.14;
let b: bool = true;
// Heap-allocated: dynamic size, managed by Box
let heap_val = Box::new(100_i32);
let heap_string = String::from("Hello, heap!");
println!("Stack: x={}, y={}, b={}", x, y, b);
println!("Heap: {}, {}", heap_val, heap_string);
// Memory freed automatically when variables go out of scope
}
Expected output:
Stack: x=42, y=3.14, b=true
Heap: 100, Hello, heap!
The RAII Pattern
Resource Acquisition Is Initialization means resources are acquired when an object is created and released when it is destroyed. In Rust, this maps perfectly to the ownership system.
struct Resource {
name: String,
}
impl Resource {
fn new(name: &str) -> Resource {
println!("Acquiring resource: {}", name);
Resource { name: String::from(name) }
}
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Releasing resource: {}", self.name);
}
}
fn main() {
let r1 = Resource::new("database connection");
let r2 = Resource::new("file handle");
println!("Working with resources...");
// r1 and r2 are dropped here in reverse order (r2 then r1)
}
Expected output:
Acquiring resource: database connection
Acquiring resource: file handle
Working with resources...
Releasing resource: file handle
Releasing resource: database connection
Ownership Transfer
When a value is moved, ownership transfers to the new owner. The old owner can no longer use the value.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership moved from s1 to s2
// println!("{}", s1); // COMPILE ERROR: s1 is no longer valid
println!("s2 = {}", s2); // s2 is the new owner
// Transfer via function call
let s3 = String::from("world");
take_ownership(s3);
// println!("{}", s3); // COMPILE ERROR: ownership moved into function
}
fn take_ownership(s: String) {
println!("Took ownership of: {}", s);
} // s is dropped here
Expected output:
s2 = hello
Took ownership of: world
Box for Heap Allocation
Box<T> is the simplest way to allocate on the heap. It is useful for recursive types and large data that should not be copied.
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
// Recursive type requires Box for known size
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("List: {:?}", list);
// Large allocation on heap
let large_vec = Box::new([0u8; 1024 * 1024]); // 1MB on heap
println!("Allocated 1MB on heap, first byte: {}", large_vec[0]);
}
Expected output:
List: Cons(1, Box(Cons(2, Box(Cons(3, Box(Nil))))))
Allocated 1MB on heap, first byte: 0
Common Mistakes
1. Assuming Heap Allocation Is Automatic
Primitives and fixed-size arrays go on the stack. Only types with dynamic size (String, Vec, Box) use the heap. Do not assume every allocation is on the heap.
2. Forgetting the Drop Order
Values are dropped in reverse declaration order (LIFO). Resources that depend on each other must be declared in the correct order to avoid use-after-free in drop implementations.
3. Unnecessary clone() Instead of Borrowing
Cloning heap-allocated data like String or Vec is expensive. Prefer references when you only need read access.
4. Using Box::new() for Large Stack Types
Box::new() allocates on the heap but the value is first created on the stack and then moved. Use Box::new() only for values that need heap allocation, not as a performance optimization.
5. Not Implementing Drop for Resource Types
If you create a type that manages a system resource (file handle, socket, mutex), implement Drop to release it. Relying on the default drop may not handle cleanup properly.
Practice Questions
1. What is the difference between stack and heap allocation? Stack allocation is LIFO, fast, and requires compile-time known sizes. Heap allocation is flexible, supports dynamic sizes, but has allocation overhead. Rust manages both automatically.
2. What does RAII stand for and how does Rust implement it?
Resource Acquisition Is Initialization. Rust implements it through ownership and Drop: resources are acquired in constructors and released when the owner goes out of scope, calling Drop::drop().
3. What happens when ownership of a value is moved? The original owner can no longer access the value. The new owner is responsible for cleanup. This prevents double frees and use-after-free errors.
4. When should you use Box<T>?
For recursive types (linked lists, trees), allocating large data on the heap, type erasure (trait objects), and reducing stack frame size by moving large values to the heap.
5. Challenge: Create a DatabaseConnection struct that prints "Connected" on creation and "Disconnected" on drop, then demonstrate ownership transfer between functions.
Mini Project: RAII Logger
use std::fs::File;
use std::io::Write;
struct Logger {
file: Option<File>,
}
impl Logger {
fn new(path: &str) -> Logger {
let file = File::create(path).expect("Cannot create log file");
println!("Logger opened: {}", path);
Logger { file: Some(file) }
}
fn log(&mut self, message: &str) {
if let Some(ref mut f) = self.file {
writeln!(f, "{}", message).expect("Write failed");
}
println!("LOG: {}", message);
}
}
impl Drop for Logger {
fn drop(&mut self) {
if self.file.is_some() {
println!("Logger closing file");
}
}
}
fn main() {
let mut logger = Logger::new("app.log");
logger.log("System initialized");
logger.log("Memory management tutorial complete");
}
FAQ
Related Concepts
What's Next
Now that you understand memory management, learn Rust Ownership for the complete guide, then master Borrowing & References.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro