⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.1516+8a4b5424dWhat's newOn an older Zig?

The Stack

The last chapter said every value has an address. This one answers the obvious follow-up: who decides which address, and for how long?

For a local variable, the machine does, and the answer is startlingly cheap. It keeps one number: where the used part of memory currently ends. Calling a function moves that mark along to make room for the call’s locals, and returning moves it back. That is the whole mechanism. Making room costs one addition, and giving it back costs one subtraction, which is why calling a function is not something you have to budget for.

The region a call gets is its frame, and the run of frames is the stack. Think of a stack of plates only in one respect: the one you put down last is the one you pick up first. A call cannot return before the calls it made have returned, so frames are always given back in the reverse of the order they were taken.

The consequence is the part that matters, and it is the source of an entire family of bugs: when the call returns, that memory is not yours any more. It was not erased. It was not marked. The mark simply moved back, and the next call to happen will be handed the same bytes.

The program

const std = @import("std");

var main_local: usize = 0;
var outer_local: usize = 0;
var inner_local: usize = 0;
var outer_local_again: usize = 0;

fn inner() void {
    var value: u32 = 3;
    _ = &value; // keep it in memory rather than a register
    inner_local = @intFromPtr(&value);
}

fn outer(record: *usize) void {
    var value: u32 = 2;
    _ = &value;
    record.* = @intFromPtr(&value);
    inner();
}

pub fn main(init: std.process.Init) !void {
    var buf: [1024]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &buf);
    const out = &stdout_writer.interface;

    var value: u32 = 1;
    _ = &value;
    main_local = @intFromPtr(&value);

    outer(&outer_local);

    // Each call gets its own region for its locals. Three calls are live at
    // once here, and no two of them share a spot.
    try out.print("main and outer used different addresses:  {}\n", .{main_local != outer_local});
    try out.print("outer and inner used different addresses: {}\n", .{inner_local != outer_local});

    // Which direction the regions run is the target's business, not a rule of
    // the language. Most machines you will meet count downward; this one does
    // not, and nothing in the chapter depends on the answer.
    try out.print("deeper calls got higher addresses here:   {}\n", .{inner_local > main_local});

    // outer() has returned, so the region it was using is no longer spoken for.
    outer(&outer_local_again);
    try out.print("\nthe second call to outer reused the same address: {}\n", .{outer_local == outer_local_again});

    try out.print("\nso a pointer to outer's local, kept after it returned,\n", .{});
    try out.print("would now be pointing at the second call's data.\n", .{});

    try out.flush();
}

What just happened

Three calls were live at once and none of them shared an address. main called outer, which called inner, and each had a local sitting somewhere of its own. Nothing coordinated that. Each call just moved the mark along by the size of what it needed.

The direction printed here is not a rule. On this target the frames run upward. On most machines you will use they run downward, which is why you will read “the stack grows down” everywhere. It is a detail of the target, the program cannot observe it without going looking, and nothing above depends on the answer. It is in the output because the honest thing to print is what actually happened rather than what the textbook says.

The second call to outer landed on the same address as the first. This is the line to remember. The first call finished, the mark went back, and the next call to need that much room was handed exactly the same bytes. The old values were still sitting there right up until they were overwritten.

So if outer had returned a pointer to its local, that pointer would still look perfectly fine. It would point at real, readable memory. It would just be memory that now belongs to somebody else, and reading it would give you a number that used to mean something. Writing through it would corrupt whatever call is using that frame now.

This is called a dangling pointer, and notice that nothing about the pointer changed. The pointer is a number, and the number is still valid. What changed is who the memory belongs to, and a number cannot carry that.

Check yourself

If outer had a second local, say var other: u32 = 9; declared next to value, would other be at a higher or lower address than value?

Neither answer is wrong to guess, because the answer is that you cannot rely on it. The compiler lays out a frame however it likes, and may not give a local an address at all if it can keep the value in a register. That is why the program above writes _ = &value; next to each one: taking the address forces the value into memory so there is something to print. Without that line, an optimising build would be entitled to make the whole question meaningless.

If you have written C

Identical machinery, and the same bug is available:

int *broken(void) {
    int x = 42;
    return &x;      /* returns a pointer into a frame that is about to end */
}

C compilers usually warn about that exact shape. Neither language stops you from doing it in a less obvious way, for example by storing the address in a struct field and returning the struct. Zig will not catch it either, and it is one of the few places where Zig’s answer is “be careful” rather than a check. The Pointers chapter says so directly: a slice of a local array must not outlive that array, and the compiler will not tell you.

What Zig gives you instead is defer and the ownership conventions in the next chapter, which between them make the question “who owns this and how long does it live” something you answer once, in a signature, rather than in every caller.

Next: Who Owns This Memory, which is about the memory that has to outlive the call.