# Pointers and Optional Pointers — Zig

Source: https://www.geekswithgeeks.com/en/zig/c-pointers

> Use single-item, many-item and optional pointers and understand const.

## Pointers without the footguns

Zig distinguishes kinds of pointers in the type system. **`*T`** points to exactly one item: take an address with `&x` and dereference with **`ptr.*`**. **`[*]T`** is a **many-item pointer** with unknown length (mainly for C interop), and **slices** `[]T` are the safe, length-carrying alternative. **`*const T`** points to data you may not modify through it, and function parameters are immutable by default, so pass `*T` when a function must modify its argument. Ordinary pointers **cannot be null**: a pointer that may be absent is an **optional pointer** **`?*T`**, which must be unwrapped (`if (p) |ptr|`, `orelse`, or `.?`, which panics if null) before use, eliminating a whole class of null dereference bugs at compile time. Struct field access auto-dereferences single pointers (`ptr.field`). `@ptrCast`, `@intFromPtr` and alignment annotations (`*align(16) T`) exist for low-level work. Zig does not have references or a borrow checker: you are responsible for **lifetimes**, so never return a pointer to a local variable, and be careful that slices into a growing `ArrayList` can be invalidated when it reallocates.

## Pointers, const and optionals

Modifying through *T, read-only *const T and nullable ?*T.

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

const Account = struct {
    owner: []const u8,
    balance_paise: u64,
};

fn deposit(account: *Account, amount: u64) void {        // *T: may modify
    account.balance_paise += amount;                     // auto-dereference for fields
}

fn describe(account: *const Account) void {              // *const T: read-only
    std.debug.print("{s}: {d} paise\n", .{ account.owner, account.balance_paise });
}

fn findAccount(accounts: []Account, owner: []const u8) ?*Account {
    for (accounts) |*acc| {                              // capture a pointer to each element
        if (std.mem.eql(u8, acc.owner, owner)) return acc;
    }
    return null;
}

pub fn main() void {
    var accounts = [_]Account{
        .{ .owner = "Asha", .balance_paise = 100_000 },
        .{ .owner = "Ravi", .balance_paise = 5_000 },
    };

    if (findAccount(&accounts, "Ravi")) |ravi| {
        deposit(ravi, 20_000);
        describe(ravi);                                  // Ravi: 25000 paise
    }

    const missing = findAccount(&accounts, "Meera");
    const balance = if (missing) |m| m.balance_paise else 0;
    std.debug.print("Meera balance: {d}\n", .{balance});

    var x: u32 = 10;
    const px = &x;
    px.* += 5;                                           // explicit dereference
    std.debug.print("x={d}\n", .{x});                    // 15
}
```

## Never return pointers to locals

A local variable lives on the stack frame of its function. Returning `&local` leaves a dangling pointer; Zig catches some cases at compile time, but not all. Return values by copy, or allocate with an allocator the caller frees.

**Quiz:** How do you represent a pointer that may be null in Zig?

- [x] With an optional pointer ?*T, which must be unwrapped before use
- [ ] *T can always be null
- [ ] With [*]T
- [ ] With undefined

*Answer:* With an optional pointer ?*T, which must be unwrapped before use. Plain pointers are non-null; ?*T makes nullability explicit and checked.
