Pointer arithmetic means forming a new pointer by moving from one element to another.
Pointer arithmetic means forming a new pointer by moving from one element to another.
In Zig, ordinary single-item pointers do not support pointer arithmetic.
var x: i32 = 10;
const p: *i32 = &x;
// p + 1 is not the right operation hereThe type *i32 means one i32. There is no second item promised by the type.
Many-item pointers do support pointer arithmetic.
var items = [_]i32{ 10, 20, 30 };
const p: [*]i32 = &items;
const q = p + 1;The pointer q points one element after p.
q[0] // same as p[1]Here is a complete program.
const std = @import("std");
pub fn main() void {
var items = [_]i32{ 10, 20, 30 };
const p: [*]i32 = &items;
const q = p + 1;
std.debug.print("{d}\n", .{q[0]});
}It prints:
20The addition is measured in elements, not bytes. Since p points to i32, p + 1 moves by one i32.
Subtraction works in the same way.
const r = q - 1;Now r points to the same item as p.
Pointer arithmetic does not carry bounds. If p points to the first element of a three-element array, then p + 3 points just past the last element. It must not be read as an item.
const end = p + 3;
// end[0] is outside the arrayA pointer past the end may be useful as a marker. It is not a pointer to a valid element.
Because many-item pointers do not store a length, the program must know the bounds by some other means.
fn printMany(ptr: [*]const i32, len: usize) void {
var i: usize = 0;
while (i < len) : (i += 1) {
std.debug.print("{d}\n", .{ptr[i]});
}
}The pointer alone is not enough. The length is a separate argument.
Slices make this safer.
fn printSlice(items: []const i32) void {
for (items) |item| {
std.debug.print("{d}\n", .{item});
}
}A slice contains both pointer and length. Most ordinary Zig code should use a slice instead of doing pointer arithmetic.
Pointer arithmetic is mainly useful near low-level interfaces: C APIs, manual memory handling, device buffers, and algorithms that are written directly over raw memory.
A byte pointer is often used when the program wants to move by bytes.
var bytes = [_]u8{ 1, 2, 3, 4 };
const p: [*]u8 = &bytes;
const second = p + 1;Here second points to the second byte.
For a larger type, pointer movement is still by elements.
var nums = [_]u32{ 1, 2, 3 };
const p: [*]u32 = &nums;
const next = p + 1;next points to the second u32, not the next byte.
Indexing is often clearer than addition.
p[2]This means the same item as:
(p + 2)[0]Prefer the first form unless the pointer itself must move.
A common pattern is to advance a pointer while a count decreases.
fn sumMany(ptr: [*]const i32, len: usize) i32 {
var p = ptr;
var n = len;
var total: i32 = 0;
while (n != 0) : ({
p += 1;
n -= 1;
}) {
total += p[0];
}
return total;
}This works, but the slice version is usually better.
fn sumSlice(items: []const i32) i32 {
var total: i32 = 0;
for (items) |item| {
total += item;
}
return total;
}The slice version says less about addresses and more about the data.
Pointer arithmetic can also be used with sentinel pointers.
fn lenZ(ptr: [*:0]const u8) usize {
var p = ptr;
var n: usize = 0;
while (p[0] != 0) : ({
p += 1;
n += 1;
}) {}
return n;
}The sentinel value 0 marks the end. The pointer moves one byte at a time until that value is found.
This is the shape of many C string operations. Zig can express it, but ordinary Zig strings are usually slices:
[]const u8A slice already has a length, so no sentinel scan is needed.
Pointer arithmetic is a low-level operation. It is useful when the type and the surrounding code provide enough information to prove that the movement is valid.
The rule is simple: move only inside storage that really contains adjacent elements, and never read outside the valid range.
Exercise 5-17. Create an array of four integers and use a many-item pointer to print the second and third elements.
Exercise 5-18. Write sumMany using pointer arithmetic and a length.
Exercise 5-19. Rewrite sumMany using a slice.
Exercise 5-20. Write a function that counts bytes in a [*:0]const u8 until it reaches the zero sentinel.