Zig Structs

A struct groups related data into a single named type. Instead of tracking a person's name, age, and email as three separate variables, you define a Person struct that holds all three together. Structs let you model real-world entities clearly and pass them around your program as a single unit.

Defining a Struct

const Point = struct {
    x: f32,
    y: f32,
};
  Point struct layout in memory:
  +-------+-------+
  |   x   |   y   |
  | (f32) | (f32) |
  +-------+-------+
  |-- 4B --|-- 4B--|
     Total: 8 bytes

Creating Struct Instances

const origin = Point{ .x = 0.0, .y = 0.0 };
const peak   = Point{ .x = 3.0, .y = 4.0 };

Each field name is prefixed with a dot when initializing. Zig requires you to provide every field — leaving one out causes a compile error unless you define default values.

Default Field Values

const Config = struct {
    width: u32  = 800,
    height: u32 = 600,
    fullscreen: bool = false,
};

const default_cfg = Config{};                   // uses all defaults
const custom_cfg  = Config{ .width = 1920, .height = 1080 }; // fullscreen stays false

Accessing Fields

const player = Point{ .x = 5.0, .y = 12.0 };
std.debug.print("x={d}, y={d}\n", .{player.x, player.y});

Use dot notation to access any field. Mutating a field requires the struct to be a var:

var score_board = struct { hits: u32, misses: u32 }{ .hits = 0, .misses = 0 };
score_board.hits += 1;

Struct Methods

Zig structs can have functions defined inside them. These functions act as methods when called with the dot notation on an instance:

const Rectangle = struct {
    width: f32,
    height: f32,

    fn area(self: Rectangle) f32 {
        return self.width * self.height;
    }

    fn perimeter(self: Rectangle) f32 {
        return 2 * (self.width + self.height);
    }
};

const room = Rectangle{ .width = 5.0, .height = 3.0 };
std.debug.print("Area: {d}\n",      .{room.area()});      // 15
std.debug.print("Perimeter: {d}\n", .{room.perimeter()});  // 16
  Rectangle{ width=5, height=3 }
         |
       .area()
         |
    5 × 3 = 15

The first parameter self receives the struct instance the method is called on. You can name it anything, but self is the convention.

Mutating Methods

A method that changes fields requires a pointer to the struct as its receiver:

const Counter = struct {
    value: u32 = 0,

    fn increment(self: *Counter) void {
        self.value += 1;
    }

    fn reset(self: *Counter) void {
        self.value = 0;
    }
};

var c = Counter{};
c.increment();
c.increment();
c.increment();
std.debug.print("Count: {d}\n", .{c.value});  // 3
c.reset();
std.debug.print("Count: {d}\n", .{c.value});  // 0
  c.value = 0
  c.increment() → c.value = 1
  c.increment() → c.value = 2
  c.increment() → c.value = 3
  c.reset()     → c.value = 0

The *Counter parameter is a pointer, meaning the method modifies the actual struct, not a copy. Without the pointer, any changes inside the method would vanish when the method returns.

Nested Structs

const Address = struct {
    city: []const u8,
    pincode: u32,
};

const Employee = struct {
    name: []const u8,
    age: u8,
    address: Address,
};

const emp = Employee{
    .name = "Meera",
    .age = 30,
    .address = Address{ .city = "Pune", .pincode = 411001 },
};

std.debug.print("{s} lives in {s}\n", .{emp.name, emp.address.city});
  Employee
  ├── name: "Meera"
  ├── age: 30
  └── address
      ├── city: "Pune"
      └── pincode: 411001

Packed Structs

By default Zig may add padding bytes between fields to align them for the CPU. A packed struct removes all padding, placing fields side by side with no gaps:

const Flags = packed struct {
    is_active:  bool,  // 1 bit
    is_admin:   bool,  // 1 bit
    is_premium: bool,  // 1 bit
    _padding:   u5,    // 5 bits to fill the byte
};
// Total: exactly 1 byte
  Normal struct bool: 1 byte per bool (compiler pads for alignment)
  Packed struct bool: 1 bit per bool (no padding between fields)

  Flags (packed):
  Bit: [ 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 ]
         [  padding  ] [prem][admn][actv]

Packed structs are essential for network protocols, hardware registers, and file formats where every bit position is specified precisely.

Extern Structs

When interfacing with C code, use extern struct to guarantee the same memory layout that C would produce:

const CPoint = extern struct {
    x: i32,
    y: i32,
};

Practical Example: Bank Account

const std = @import("std");

const Account = struct {
    owner: []const u8,
    balance: f64,

    fn deposit(self: *Account, amount: f64) void {
        self.balance += amount;
        std.debug.print("Deposited {d:.2}. Balance: {d:.2}\n",
            .{amount, self.balance});
    }

    fn withdraw(self: *Account, amount: f64) bool {
        if (amount > self.balance) {
            std.debug.print("Insufficient funds.\n", .{});
            return false;
        }
        self.balance -= amount;
        std.debug.print("Withdrawn {d:.2}. Balance: {d:.2}\n",
            .{amount, self.balance});
        return true;
    }
};

pub fn main() void {
    var acc = Account{ .owner = "Deepak", .balance = 1000.0 };
    acc.deposit(500.0);
    _ = acc.withdraw(200.0);
    _ = acc.withdraw(2000.0);
}

Output:

Deposited 500.00. Balance: 1500.00
Withdrawn 200.00. Balance: 1300.00
Insufficient funds.

Leave a Comment

Your email address will not be published. Required fields are marked *