Pointer arithmetic
In Raw memory, a pointer from Alloc was indexed like an array: values[i]. That works because a pointer can do arithmetic. Adding a number to it gives a new pointer further along in memory — and the step is measured in whole elements, not in bytes.
A step is one element
The program takes the address of the first element of three arrays and measures how far + 1 moves each pointer. Converting an address to uint shows it as a plain number of bytes, so two addresses can be subtracted:
let b = @bytes[0];
let c = @counts[0];
let x = @pixels[0];
PrintLine("+1 on *uint8 moves {} byte", ((b + 1) as uint) - (b as uint));
PrintLine("+1 on *int64 moves {} bytes", ((c + 1) as uint) - (c as uint));
PrintLine("+1 on *Pixel moves {} bytes", ((x + 1) as uint) - (x as uint));
| Pointer | One element is | + 1 moves |
|---|---|---|
*uint8 | one byte | 1 byte |
*int64 | eight bytes | 8 bytes |
*Pixel | three int32s together | 12 bytes |
The compiler multiplies by the element size for you. You always count in elements, whatever their size.
Offset and index are one thing
Indexing a pointer is just arithmetic followed by a dereference: p[i] means exactly *(p + i).
PrintLine("*(c + 2) is {}, c[2] is {}", *(c + 2), c[2]);
PrintLine("(x + 1).blue is {}", (x + 1).blue);
A field is reached through the moved pointer with a plain ., as in Pointer.
Walking to an end pointer
A pointer can also walk. The loop below keeps a second pointer, end, one past the last element:
var cursor = c;
let end = c + 4;
Print("doubled:");
while cursor < end {
*cursor = *cursor * 2;
Print(" {}", *cursor);
cursor += 1;
}
flowchart LR
c0["c<br/>10"] --- c1["c + 1<br/>20"] --- c2["c + 2<br/>30"] --- c3["c + 3<br/>40"] --- e["end = c + 4<br/>never read"]Pointers compare by address, so cursor < end means "not there yet". end itself is never read through: it marks the place just past the array. Because cursor points into counts, the doubled values are written into the array itself, which the last line confirms.
Nothing is checked
The arithmetic is never checked. A pointer moved past the end of its storage is still a pointer, and reaching through it reads or writes memory that belongs to something else — another variable, or nothing at all. Keep an end in view, as the loop does, and never step past it.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Adding a number to a pointer moves it by whole elements, not by bytes. If `p` points at an
// `int64`, then `p + 1` points at the next `int64`, eight bytes further on. The compiler
// multiplies by the element size for you, so the same `+ 1` steps one byte through `uint8`s
// and twelve bytes through a twelve-byte struct.
//
// That is also what indexing a pointer means: `p[i]` is exactly `*(p + i)`.
//
// The arithmetic itself is never checked. A pointer moved past the end of its storage is still
// a pointer, and reaching through it reads or writes memory that belongs to something else.
// Keep an end in view: here, a second pointer one past the last element.
import Io::{ Print, PrintLine };
struct Pixel {
red: int32;
green: int32;
blue: int32;
}
func Main() -> int {
var bytes: uint8[4] = [1, 2, 3, 4];
var counts: int64[4] = [10, 20, 30, 40];
var pixels: Pixel[2] = [Pixel { red: 255, green: 0, blue: 0 },
Pixel { red: 0, green: 0, blue: 255 }];
// Converting an address to `uint` shows it as a number of bytes, so the step `+ 1` takes
// can be measured. It is the element size each time.
let b = @bytes[0];
let c = @counts[0];
let x = @pixels[0];
PrintLine("+1 on *uint8 moves {} byte", ((b + 1) as uint) - (b as uint));
PrintLine("+1 on *int64 moves {} bytes", ((c + 1) as uint) - (c as uint));
PrintLine("+1 on *Pixel moves {} bytes", ((x + 1) as uint) - (x as uint));
// Offset and index are two spellings of one thing.
PrintLine("*(c + 2) is {}, c[2] is {}", *(c + 2), c[2]);
PrintLine("(x + 1).blue is {}", (x + 1).blue);
// Walking with a pointer: `end` is one past the last element and is never read through.
// Pointers compare by address, so `cursor < end` means "not there yet".
var cursor = c;
let end = c + 4;
Print("doubled:");
while cursor < end {
*cursor = *cursor * 2;
Print(" {}", *cursor);
cursor += 1;
}
PrintLine();
// The writes went through to the array itself.
PrintLine("counts[3] is now {}", counts[3]);
return 0;
}
Run it
cd Examples/Memory/PointerArithmetic
rux run
+1 on *uint8 moves 1 byte
+1 on *int64 moves 8 bytes
+1 on *Pixel moves 12 bytes
*(c + 2) is 30, c[2] is 30
(x + 1).blue is 255
doubled: 20 40 60 80
counts[3] is now 80
Common mistakes
To skip one
int64 you add 1, not 8. c + 8 compiles and moves 64 bytes — far past the end of a four-element array.*c + 2 reads the first element and adds 2 to it, giving 12. To reach two elements on, write *(c + 2), or simply c[2].while cursor <= end runs once too often and reads one element past the array. Nothing reports it. end marks the stopping place; compare with <.end - c is not allowed: it fails with error: operator '-' cannot combine left operand '*var int64' with right operand '*var int64'. To count the elements between them, convert both to uint, subtract, and divide by sizeof(int64).Try it yourself
- Print the counts in reverse: start a cursor at
end, and in awhile cursor > cloop step it back withcursor -= 1before each read. - Count the elements between
candendwith((end as uint) - (c as uint)) / sizeof(int64). - Walk
pixelswith a cursor and print each pixel'sredandblue.
Learn more
- Pointer arithmetic in the Rux Reference
- Raw memory — where indexing a pointer first appeared
- Pointer slice — keeping the length beside the pointer, so the end is checked for you