# Stack vs Heap

### Stack vs Heap

Zig programs use memory in different places. The two most important places are the stack and the heap.

The stack is used for local values and function calls.

The heap is used for memory requested from an allocator.

Both are ordinary memory, but they are managed differently. Understanding this difference is necessary before you write programs that allocate buffers, build dynamic arrays, return data from functions, or manage long-lived objects.

#### Stack Memory

Stack memory is used automatically when functions run.

Example:

```zig
pub fn main() void {
    const x: i32 = 10;
    const y: i32 = 20;
    const z = x + y;

    _ = z;
}
```

Here, `x`, `y`, and `z` are local values. They live while `main` is running.

You do not allocate them manually.

You do not free them manually.

When the function exits, the stack space used by those local values is no longer valid.

That is the basic stack rule:

Local stack data lives only inside its scope.

#### A Stack Array

This array is stored as a local value:

```zig
pub fn main() void {
    var buffer: [16]u8 = undefined;

    buffer[0] = 1;
    buffer[1] = 2;

    _ = buffer;
}
```

The array has a fixed size known at compile time:

```zig
[16]u8
```

It stores 16 bytes.

This is simple and fast. No allocator is involved.

Stack memory is good for small, fixed-size, temporary data.

#### Stack Memory Has a Lifetime

A local value becomes invalid when its scope ends.

This function is wrong:

```zig
fn bad() *i32 {
    var x: i32 = 10;
    return &x;
}
```

The function returns a pointer to `x`.

But `x` lives on the stack inside `bad`. When `bad` returns, `x` is gone. The returned pointer points to invalid memory.

This is called a dangling pointer.

The problem is not the pointer syntax. The problem is lifetime.

The pointer outlives the value it points to.

#### Heap Memory

Heap memory is requested explicitly from an allocator.

Example:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const buffer = try allocator.alloc(u8, 16);
    defer allocator.free(buffer);

    buffer[0] = 1;
    buffer[1] = 2;
}
```

This line asks for heap memory:

```zig
const buffer = try allocator.alloc(u8, 16);
```

It returns a slice:

```zig
[]u8
```

The slice views 16 `u8` values stored in heap memory.

This line releases the memory:

```zig
defer allocator.free(buffer);
```

Heap memory stays valid until it is freed.

That is the basic heap rule:

Allocated memory lives until you free it, or until the allocator is destroyed.

#### Why Heap Memory Exists

Stack memory is easy, but it has limits.

Use heap memory when:

The size is known only at runtime.

The data must live after the current function returns.

The data may grow or shrink.

The data is too large for comfortable stack use.

Many objects need to share ownership rules through a larger program.

Example:

```zig
const buffer = try allocator.alloc(u8, count);
```

Here, `count` may be computed at runtime. The compiler may not know the size in advance. Heap allocation handles that.

#### Returning Heap Memory

A function can return heap-allocated memory, but the ownership must be clear.

```zig
const std = @import("std");

fn makeBuffer(allocator: std.mem.Allocator, len: usize) ![]u8 {
    const buffer = try allocator.alloc(u8, len);
    return buffer;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const buffer = try makeBuffer(allocator, 32);
    defer allocator.free(buffer);

    buffer[0] = 42;
}
```

This is valid because `makeBuffer` returns memory that was allocated from the heap.

But the caller must free it.

The function should be understood as saying:

I allocate memory for you. You now own it.

In Zig, that ownership rule should be documented by the function name, parameters, and surrounding code.

#### Returning Stack Memory Is Wrong

Compare the heap version with this wrong stack version:

```zig
fn makeBadBuffer() []u8 {
    var buffer: [32]u8 = undefined;
    return buffer[0..];
}
```

This returns a slice into a local array.

The array dies when the function returns.

The returned slice points to invalid memory.

This is one of the most important mistakes to avoid in Zig.

Never return a pointer or slice to local stack data.

#### Caller-Provided Stack Memory

Often, the best design is to let the caller provide the memory.

```zig
fn writeHello(buffer: []u8) []u8 {
    buffer[0] = 'h';
    buffer[1] = 'i';
    return buffer[0..2];
}
```

Use it like this:

```zig
const std = @import("std");

fn writeHello(buffer: []u8) []u8 {
    buffer[0] = 'h';
    buffer[1] = 'i';
    return buffer[0..2];
}

pub fn main() void {
    var storage: [16]u8 = undefined;

    const result = writeHello(storage[0..]);

    std.debug.print("{s}\n", .{result});
}
```

Output:

```text
hi
```

No heap allocation is needed.

The caller owns the storage. The function only borrows it.

This is a common Zig style. It makes memory ownership simple and visible.

#### Stack vs Heap Table

| Feature | Stack | Heap |
|---|---|---|
| Managed by | Function calls and scopes | Allocators |
| Size | Usually fixed and known locally | Can be decided at runtime |
| Lifetime | Ends with scope or function | Ends when freed or allocator is destroyed |
| Speed | Very fast | Usually slower than stack |
| Manual free needed | No | Yes |
| Good for | Small temporary values | Dynamic or long-lived data |
| Common type | `[N]T`, local `struct`, local variables | `[]T`, allocated objects, dynamic containers |

#### Allocation Can Fail

Stack local variables usually do not return errors when created.

Heap allocation can fail.

```zig
const buffer = try allocator.alloc(u8, 1024);
```

The `try` is necessary because the allocator may not be able to provide the requested memory.

That means heap allocation affects function signatures.

If a function allocates, it often returns an error union:

```zig
fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.alloc(u8, 1024);
}
```

The `![]u8` return type means:

This function either returns a `[]u8`, or it returns an error.

Memory allocation is not invisible in Zig.

#### Defer Is Common with Heap Memory

When you allocate memory, you often use `defer` to free it.

```zig
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
```

`defer` means:

Run this cleanup when the current scope exits.

This is useful because it keeps allocation and cleanup close together.

Example:

```zig
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);

// use buffer here
```

Even if the function later returns early with an error, the deferred cleanup still runs.

This reduces leaks.

#### Ownership Transfer

Sometimes a function allocates memory and returns it. In that case, it should not free it before returning.

```zig
fn makeName(allocator: std.mem.Allocator) ![]u8 {
    const name = try allocator.alloc(u8, 3);

    name[0] = 'z';
    name[1] = 'i';
    name[2] = 'g';

    return name;
}
```

Do not write this:

```zig
fn makeNameBad(allocator: std.mem.Allocator) ![]u8 {
    const name = try allocator.alloc(u8, 3);
    defer allocator.free(name);

    return name;
}
```

That version returns memory and then frees it as the function exits. The caller receives a slice to memory that has already been freed.

The correct rule is:

If you return allocated memory, the caller usually becomes responsible for freeing it.

#### Heap Memory and Containers

Many standard library containers use heap memory.

For example, an `ArrayList` grows dynamically, so it needs an allocator.

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var list = std.ArrayList(u8).init(allocator);
    defer list.deinit();

    try list.append(10);
    try list.append(20);
    try list.append(30);

    std.debug.print("{any}\n", .{list.items});
}
```

The `ArrayList` owns heap memory internally.

This line cleans it up:

```zig
defer list.deinit();
```

The pattern is the same:

Create something that owns memory.

Use it.

Deinitialize it.

#### Large Stack Values

The stack is fast, but not unlimited.

This may be a bad idea:

```zig
pub fn main() void {
    var huge: [100_000_000]u8 = undefined;
    _ = huge;
}
```

That asks for a very large local array.

Large buffers usually belong on the heap, or inside a long-lived allocator strategy.

```zig
const huge = try allocator.alloc(u8, 100_000_000);
defer allocator.free(huge);
```

Use stack memory for small, local data. Use heap memory for large or dynamic data.

#### The Cost of Heap Allocation

Heap allocation is flexible, but it has costs.

It can fail.

It is usually slower than stack allocation.

It requires cleanup.

It can fragment memory.

It makes ownership more complicated.

This does not mean heap allocation is bad. It means heap allocation should be intentional.

A good Zig program does not allocate casually. It chooses where memory lives.

#### Common Mistake: Allocating When a Stack Buffer Is Enough

This is unnecessary:

```zig
const buffer = try allocator.alloc(u8, 32);
defer allocator.free(buffer);
```

if the buffer is fixed, small, and local.

Use:

```zig
var buffer: [32]u8 = undefined;
```

Then pass it as a slice if needed:

```zig
useBuffer(buffer[0..]);
```

This avoids allocation entirely.

#### Common Mistake: Forgetting to Free

This leaks memory:

```zig
const buffer = try allocator.alloc(u8, 1024);
buffer[0] = 1;
```

The program allocated memory and never released it.

Correct:

```zig
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);

buffer[0] = 1;
```

When a function owns allocated memory, it needs a cleanup path.

#### Common Mistake: Freeing Too Early

This is also wrong:

```zig
const buffer = try allocator.alloc(u8, 1024);
allocator.free(buffer);

buffer[0] = 1;
```

After `free`, the memory is no longer yours.

Using it after freeing is a use-after-free bug.

The rule is direct:

After memory is freed, no pointer or slice to it should be used.

#### Main Idea

Stack memory is automatic and tied to scope.

Heap memory is explicit and tied to an allocator.

Use the stack for small, fixed-size, temporary values.

Use the heap for dynamic, large, or longer-lived data.

Do not return pointers or slices to local stack data.

When you allocate heap memory, make ownership clear and free it exactly once.

