[Avg. reading time: 15 minutes]

Stack - Heap

1

Rust guarantees memory safety without a garbage collector by enforcing rules about ownership, borrowing, and lifetimes at compile time.

Most Rust programs use these memory regions:

  • Static (Data) memory: values that exist for the entire program run
  • Stack: per-function call frames, fast, fixed-size data
  • Heap: dynamic, resizable data allocated at runtime

Static/Data Memory

This region holds data that is available for the entire lifetime of the program.

Typical residents:

  • Program code (binary / text segment)
  • Static variables (static)
  • String literals (read-only bytes baked into the binary)

Example

fn main() {
    println!("Hello");
}

The bytes for “Hello” live in static memory. The program just references them.

A typical 64bit Windows

High Address
|---------------------|
|        Stack        |  ~1 MB default (for each thread)
|---------------------|
|                     |
|         Heap        |  grows dynamically
|                     |
|---------------------|
|   Static / Data     |  fixed at compile time
|---------------------|
|   Program Code      |
|---------------------|
Low Address

Stack

Stack is LIFO (Last In First Out).

The stack stores stack frames created when functions are called. Each frame typically contains:

  • Function arguments
  • Local variables
  • Bookkeeping info (return address, saved registers)

Stack data is usually:

  • Known size at compile time
  • Fast to allocate and free
  • Automatically cleaned up when the function returns

Mac/Linux

8 MB default stack space.

ulimit -s
  • Each process may have many threads. Ex: Google Chrome uses several threads.
  • Each thread has its own stack.
  • Stack memory is reserved not used immediately.
  • Heap and Static are shared per process.
Process 1 (Chrome)
 ├── Thread 1 → Stack 1
 ├── Thread 2 → Stack 2
 ├── Thread 3 → Stack 3
 ├── Shared Heap 1
 └── Shared Static/Data 1
Process 2 (VSCode)
 ├── Thread 1 → Stack 1
 ├── Thread 2 → Stack 2
 ├── Thread 3 → Stack 3
 ├── Shared Heap 2
 └── Shared Static/Data 2
fn foo() {
    let y = 5;
    let z = 100;
}

fn main() {
    let x = 42;

    foo();
}
AddressNameValue
0x42

After calling the function foo()

AddressNameValue
2z100
1y5
0x42

Note: Memory address is taking into account DATA TYPE SIZE. It’s just a representation.

After foo() gets executed, control transfers to main, and the values are deallocated automatically.

AddressNameValue
0x42

stateDiagram-v2
    [*] --> Main
    state Main {
        [*] --> PushMainFrame
        PushMainFrame: Push frame for main()
        PushMainFrame --> AllocateX: x = 42
        AllocateX --> CallFoo: Call foo()
        CallFoo --> PushFooFrame: Push frame for foo()
        state PushFooFrame {
            [*] --> AllocateY: y = 5
            AllocateY --> AllocateZ: z = 100
        }
        PushFooFrame --> PopFooFrame: Pop frame for foo()
        PopFooFrame --> ReturnFoo: foo() returns
        ReturnFoo --> PopMainFrame: Pop frame for main()
        PopMainFrame --> [*]
    }
    Main --> [*]

Copy Trait

Types like i32, bool, char, and many small fixed-size structs implement Copy. Assignment duplicates bits.

    fn main() {
    // i32 is a simple type and are normally stored on the stack.
    // copy trait

    let x = 42;
    let y = x; 

    // The value bound to x is Copy, so no error will be raised.
    println!("{:?}", (x, y));

    // The value bound to x is Copy, so no error will be raised.
    println!("{:p},{:p}", &x, &y);
}

Heap

The heap stores data that is:

  • Allocated at runtime
  • Often resizable or variable-sized
  • Accessed via a pointer stored on the stack

Common heap-backed types:

  • String
  • Vec
  • Box
  • HashMap<K, V>
fn main(){
    let s1=String::from("hello");
    println!("{}",s1)
}


fn main() {
    let s = String::from("rust");

    println!("Value        : {}", s);
    println!("Length (bytes): {}", s.len());
    println!("Capacity     : {}", s.capacity());
    println!("Stack addr   : {:p}", &s);
    println!("Heap ptr     : {:p}", s.as_ptr());
}

// English and international characters

fn main() {
    let s = String::from("rust-ரஸ்ட்");

    println!("Value        : {}", s);
    println!("Length (bytes): {}", s.len());
    println!("Capacity     : {}", s.capacity());
    println!("Stack addr   : {:p}", &s);
    println!("Heap ptr     : {:p}", s.as_ptr());
}

Move Trait

Heap-backed values are usually moved, not copied, because duplicating them would require duplicating heap allocations.

// Move Trait (Heap)

fn main() {
    let mut name = String::from("Hello World");
    println!("Memory address of name: {},{:p} \n", name,&name);

    //moving
    let name1 = name;
    println!("Memory address of name1: {},{:p} \n", name1,&name1);
    //println!("Memory address of name: {},{:p} \n", name,&name);
    // Setting up another Value for the variable name
    name = String::from("Dear World");
    println!("Memory address of name: {},{:p}\n", name,&name);
}

Example: pdf (stack) - printed book (heap)

// Copy Trait (Stack - because of using String Literal)

fn main() {
    let name = "Hello World";
    println!("Memory address of name: {},{:p} \n", name,&name);

    //Copying

    let name1 = name;
    println!("Memory address of name1: {},{:p} \n", name1,&name1);
    
    println!("Memory address of name: {},{:p} \n", name,&name);
}

#stack #heap #memory


1: adafruit.comVer 2.0.15

Last change: 2026-02-25