DEV Community

Mukumbuta
Mukumbuta

Posted on

Memory Management in Zig

When transitioning to Zig from garbage-collected languages like Java or Python, or even from object-oriented languages like C++ one realization completely changes how you write code:

In Zig, memory management is deliberate, symmetrical, and completely exposed.

To become comfortable with Zig, I found that I had to internalize one simple mental model:

Every struct is responsible for the resources it owns.

Once this clicked, many parts of the language started making sense.

No Universal Cleanup
In many high-level languages, there's a runtime quietly cleaning things up after you. Objects become unreachable, the garbage collector runs, and memory gets reclaimed.

Zig doesn't have that safety net.

Conceptually, you can think of a struct as a layout of bytes in memory. It doesn't secretly inherit from a universal Object type, it doesn't carry hidden metadata, and there isn't a language-wide deinit() that every type automatically gets.

If a type knows how to clean itself up, it's because someone explicitly wrote that cleanup logic.

What list.deinit() Really Means
You'll eventually write code like this:

var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
Enter fullscreen mode Exit fullscreen mode

At first glance, it feels like you're calling a method on an object.

What's actually happening is much simpler.

The dot syntax is just a convenient way of calling a function that belongs to that type.

The compiler effectively turns something like:

list.deinit();
Enter fullscreen mode Exit fullscreen mode

into:

std.ArrayList(u8).deinit(&list);
Enter fullscreen mode Exit fullscreen mode

There's no hidden object system here.

ArrayList simply defines a function named deinit, and Zig lets you call it using dot syntax.

Different types expose different cleanup operations because different types own different resources.

For example:

  • std.ArrayList frees its dynamically allocated backing buffer.
  • A custom Person type might free a duplicated name stored on the heap.
  • std.fs.File doesn't have a deinit() method at all. Instead, it provides close(), because semantically that's what you're doing.

If A Type Owns Resources, It Should Clean Them Up
One lesson that really stuck with me is this:

If your type owns resources, it should usually expose a way to release them.

Consider a simple Person type that duplicates a name onto the heap:

const Person = struct {
    const Self = @This();

    name: []const u8,
    age: u8,
    employed: bool,

    pub fn clone(self: Self, allocator: std.mem.Allocator) !Self {
        return Self{
            .name = try allocator.dupe(u8, self.name),
            .age = self.age,
            .employed = self.employed,
        };
    }

    pub fn deinit(self: Self, allocator: std.mem.Allocator) void {
        allocator.free(self.name);
    }
};
Enter fullscreen mode Exit fullscreen mode

Notice something here... the allocator performs the allocation, but the allocator doesn't own the memory.

The cloned Person does.

That means the cloned Person is responsible for freeing it.

This was another mental shift for me:

Who allocates isn't necessarily who frees. Whoever owns the resource is responsible for releasing it.

Nested Ownership Requires Nested Cleanup
Things get even more interesting when one type owns another.

Suppose you store multiple cloned Person values inside an ArrayList:

for (list.items) |person| {
    person.deinit(allocator);
}

list.deinit();
Enter fullscreen mode Exit fullscreen mode

The cleanup happens in two stages.

First, each Person frees the heap memory that it owns.

Only after that does the ArrayList free its own backing buffer.

The important thing to realize is that the ArrayList has no idea what lives inside each Person. As far as it's concerned, it's simply storing Person values.

Likewise, each Person knows nothing about the ArrayList that happens to contain it.

Each type cleans up only the resources it owns.

The Mental Model That Changed Everything
I arrived at this understanding after chasing memory leaks, ownership bugs, and plenty of confusing behavior while learning Zig.

Eventually, the pattern became obvious:

  • Every type has a clear responsibility.
  • Every allocation should have a matching cleanup.
  • Every owner is responsible for releasing what it owns.

Once I started thinking in terms of ownership and responsibility, Zig's memory management became much easier to reason about.

If you're just getting started with Zig, I think this is one of the most valuable mental models you can develop early. It has already changed the way I think about systems programming, and I suspect it'll continue paying dividends as I continue learning.

I'd love to hear how other Zig developers think about ownership and cleanup. If you have a different mental model or best practice, let me know in the comments :)

Top comments (0)