🎓 A1Learner
🔍
0 of 55 lessons completed

Zig Freeing Memory

Freeing memory means handing allocator memory back as soon as the program is done with it. The defer statement is the usual way to make sure the free happens.

defer free sits right after the alloc

The defer line is written now and runs when main ends. The slice is filled and printed while it is still held.

const std = @import("std");

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const alloc = std.heap.page_allocator;
    const buf = try alloc.alloc(i32, 3);
    defer alloc.free(buf);
    buf[0] = 2;
    buf[1] = 4;
    buf[2] = 6;
    try out.print("{any}\n", .{buf});
}

Code keeps running after an early free

The total is copied out of the slice before the explicit free. Both prints after the free still show the saved total.

const std = @import("std");

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const alloc = std.heap.page_allocator;
    const buf = try alloc.alloc(i32, 3);
    buf[0] = 2;
    buf[1] = 3;
    buf[2] = 4;
    var total: i32 = 0;
    for (buf) |v| {
        total += v;
    }
    alloc.free(buf);
    try out.print("{d}\n", .{total});
    try out.print("{d}\n", .{total * 2});
}

A block frees its own memory at the brace

temp lives only inside the block, and its deferred free runs at the closing brace. The total copied out before that point prints after the block.

const std = @import("std");

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const alloc = std.heap.page_allocator;
    var total: i32 = 0;
    {
        const temp = try alloc.alloc(i32, 2);
        defer alloc.free(temp);
        temp[0] = 10;
        temp[1] = 20;
        try out.print("{any}\n", .{temp});
        total = temp[0] + temp[1];
    }
    try out.print("total = {d}\n", .{total});
}

Example

const std = @import("std");

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const alloc = std.heap.page_allocator;
    var rounds: i32 = 0;
    var grand: i32 = 0;
    for (0..3) |i| {
        const buf = try alloc.alloc(i32, 2);
        defer alloc.free(buf);
        const start: i32 = @intCast(i + 1);
        buf[0] = start;
        buf[1] = start * 10;
        rounds += 1;
        grand += buf[0] + buf[1];
        try out.print("round {d}: {any}\n", .{ rounds, buf });
    }
    try out.print("rounds = {d}, total = {d}\n", .{ rounds, grand });
}

Key points

  • free: alloc.free(buf) returns the memory behind buf to the allocator.
  • defer free: defer alloc.free(buf); written right after the alloc runs the free when the current scope ends.
  • Scope: a block ends at its closing brace, and deferred frees run at that point.
  • Loop use: memory allocated inside a loop body can be freed at the end of each pass through the loop.
  • Leak: memory that is never freed stays in use until the program ends.
  • Pairing: every alloc should have exactly one matching free.

In this example, each pass through the loop allocates a fresh two-item slice and defers its free, so the memory is handed back at the end of that pass. The passes build 1 and 10, then 2 and 20, then 3 and 30, and each round is printed with its count. After the loop, the prints show 3 rounds and a grand total of 66.

Note

Zig does not clean up forgotten memory for you, so a missed free is a leak only the programmer can prevent.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A line reads defer alloc.free(buf); inside a block. When does the free run?