Zig for the Uninitiated: Pointers, Arrays, and Slices
By Tyler
Hey all! Welcome to the second installment of Zig for the uninitiated! Last section I talked about process memory and how the kernel will allocate memory for processes to use. This month I want to discuss how variables are stored in memory, and how we can work with them. This will set us up for a future discussion about heaps, stack-frames.
Pointers
Whenever a variable is created, it has to be stored at a specific place in memory. Where that memory is stored is called it’s “Address”. On pretty much all computers, the basic addressable unit of memory is one Byte. This just means that any variable, including booleans will be stored in at least one byte 1. Variables also can be multiple bytes. In that case the pointer will point to the first byte. We discuss sizes more later.
Now how do we find out the address of a variable? Well that is where pointers come
in. Pointers are a type of variable that store addresses to locations in memory. We
can create a pointer by using the address of operator &
.
const age: u8 = 128; // Address: 0xffeeddccbbaa0010
const p_age: *u8 = &age; // Value: 0xffeeddccbbaa0010
Notice that the type of p_age
is *u8
. The asterisk indicates that it is
a pointer to a u8. If we had a foo
type a pointer to it would be *foo
.
Pointers are useful in zig when you want to mutate a value in a function call. In zig all function parameters are immutable. If we want to modify a value through a function, we can pass a pointer to that value. The pointer will not be able to be modified, but we can modify the data that it points to.
Pointers are also a fixed size, for a given architecture. On 64bit systems they will be 8 bytes (8 bytes * 8 bits per byte = 64 bits). This means if you have data which can be large, or even unknown in size, you can pass around a pointer to it instead.
Arrays
Arrays are simply contiguous areas of memory that store a concrete amount of data of a certain type. That means that when we create an array, we have to specify at compile time, how big the array will be.
When we create an Array we use the following syntax
const nums: [9]u8 = .{1,2,3,6,11,23,47,106,235};
// This is also accpetable
const nums = [9]u8{1,2,3,6,11,23,47,106,235};
// You can even have the compiler compute what the exact size of your list is:
// However the Size must be computable by the compiler.
const nums = [_]u8{1,2,3,6,11,23,47,106,235};
Slices
Sometimes we want to work with an array but don’t know how big it needs to be or will be. For example, what if we want to write a function that calcualtes the sum of an array of bytes. It would be impossible and absurd to write a function for every possible length of bytes.
If this sounds like a problem a pointer could fix, then you’re thinking well.
Zig has a special kind of pointer called a “Slice”. In reality the Slice is just
a data structure with two fields .ptr
, a pointer to the first element of the slice
and .len
, the number of elements in the slice.
const Slice = struct {
ptr: usize,
len: usize,
}
usize
represents the underlying type of a pointer. It is an unsized integer,
of the size of a pointer for the architecture of the computer. On 64bit computers
usizes will be the same size as a u64, 8 bytes.
Using slices we can make a single function that computes the sum of any sized array 2.
const std = @import("std");
fn sum(list: []usize) usize {
var total: usize = 0;
// Zig's for loop syntax is a bit backwards from most languages
// loop over the array, and 'capture' the item into the variable
// between the pipes
for (list) |val| {
total += val;
}
return total;
}
// Don't worry about the main function's return type just now.
// It has to do with error handling.
pub fn main() !void {
var nums = [_]usize{ 1, 2, 3, 6, 11, 23, 47, 106, 235 };
// We have to slice nums to create a Slice to pass to the function
// The 0.. syntax will make a slice with all the elements.
const result = sum(nums[0..]);
std.debug.print("Sum is: {d}\n", .{result});
}
Next steps
Next time we’ll talk about stacks, heaps, and how we work with them. If you’re interested in more about slices, arrays and pointers, watch the video above, where I discuss how this looks in memory.
-
Bit Packing is a way to get around this, by storing more than 1 variable in the same byte. For example, you can store 8 boolean variables as one byte, and then do bitwise arithmatic to access individual bits. ↩︎
-
Our example is not generic, so will only work for type usize. It is possible to make our function generic over any integer type, but that is for a different discussion. ↩︎