Chapter F:
Pointers
A pointer points to something. Let’s dive into the simplest example:
var foo: usize = 5000;
const foo_pointer: *usize = &foo;
In this example, foo is a usize
, and foo_pointer
is a *usize
(read as “a pointer to a usize”). Which usize does foo_pointer
point to? Well, it points to foo
. &foo
means “take a pointer to foo
”.
What can you do with a pointer? Well, you can read or write the data that it points to.
foo_pointer.* = 100;
std.debug.print("{} {}\n", .{ foo_pointer.*, foo });
This updates the value in foo
through the pointer, then prints that value twice, once reading through the pointer and once reading the variable directly.
Pointers can often be confusing for new programmers, since they’re a unique and powerful tool. They let you do things that you can’t otherwise do: reading and updating memory “at a distance”. For example, three different pieces of code could all have access to the same integer counter as long they each had a pointer to the same variable.
But I’m sure you won’t have much trouble, since you have prior experience using them (even if you’ve forgotten it).
Slices
Before I can talk about what a slice is, I have to talk about what a pointer is not. When I introduced pointers as a pointing to “a variable”, you made the reasonable assumption that the pointer itself contained all the information necessary to read and write from that variable. However, that’s not the case.
In pointer type, like *[5]i32
(remember, read as “a pointer to an array of 5 u32’s”), there’s an important limitation. Namely, the pointer has a comptime-known type. Since not every variable has the same size in memory, the Zig compiler needs to use this type to access the right amount of data when reading/writing through the pointer. That is to say, the pointer defines a single location in memory, and the type defines the size of the what data at that location.
However, if you need to store or access a runtime-known number of items, which is very often, you need a different solution. This is where slices come in. Slices are a pointer and a length.
Slices are a often treated as a type of pointer. Since they’re just a pointer and a bit of extra metadata, all the rules that apply to pointers apply to slices. If you see a documentation comment saying “must pass a pointer”, you can (probably) pass a slice as well. If someone says “don’t do that with a pointer!” you (probably) shouldn’t do it with a slice either.
// TODO: need an example of taking pointers into arrays foo = .{1, 2, 3} bar = &foo[1]; baz = foo[1..];
// can talk about sub-slicing slices when we talk about for loops, or here, whatever
Types of Memory
Pointers don’t have to point to variables though, they can also point to other type of memory.
YOU: There are different types of memory?
Well, it’s not quite right to say that these are different types of memory, since they are all three backed by the same physical memory block. Some diagrams will instead focus on these on different “places” in memory (you can do an image search for “memory layout” to find many images of this diagram). But this isn’t quite accurate either, as there differences to these types of memory beyond their locations in address space.
It’s most accurate to say that there are three different ways of organizing memory.
Static memory is like furniture—your bed, your lamp, the Open Sauce and Game Changer posters on your wall. Things that don’t move.
The stack is like a shelf of books or a chest of drawers. It’s linear, it’s organized, and it’s very tightly packed. I’ll talk more about The stack in the chapter on Functions, because the stack is sorted per function. The books on my bookshelf are sorted by author’s last name. The data in the stack is sorted by what function it’s associated with.
The heap is the pile of dirty laundry and half-finished books and crumpled up papers in the middle of your floor. It’s dynamic. You can put as much or as little data there as you want, and you can organize it however you want. It’s unordered, so you can add or remove to any part at any time. It’s also “sparse”—there are big empty sections. Static and stack sections take up only as much space as they need†, and then the remaining space is designated as the heap.
†This isn’t true yet. In current versions of Zig, the stack space is fixed at a reasonable amount (I think it’s 4MB). It’s a goal of future Zig versions to compute the exact maximum needed stack size at compile time in order to maximize the available heap. (Everything that isn’t stack static is heap.)
When writing Zig code, variables inside of functions automatically end up on the stack, and compile-time-known constants end up in static memory.
pub const password = "I don't remember, please help.";
fn main () {
const message = "some days are better than others, but all of them are bad.";
var a: usize = 5;
a = message.len;
}
In this example, password
, message
, and 5
, are all static values. a
is a runtime known variable (in this case, the value changes when we run the program). So a
is stored on the function stack associated with the main
function.
YOU: I think that makes sense. What about heap memory though?
Pointers to stack and static memory are similar in that they are both (normally) created by taking a reference to a variable (&some_variable
, as shown earlier). The heap is different because variables are never stored on the heap. In order to access the heap, you need to go through an allocator.
Allocators
Stack and static allocations happen automatically, corresponding to variables and constants you declare. But sometimes you need to allocate memory dynamically—you need to control the allocation. The most common reason to do this is when you need to control the size of the allocation.
TODO
Ownership
// ownership in voice memos, needs transcibed
Slices
TODO