Five ints sit side by side in memory. How does the machine find the fourth? Watch the address in p as it walks them.
Stepping through an array in memory
A pointer walks five cells; its address rises by the size of one
#include <stdio.h> int main(void){ int a[5] = { 4, 8, 15, 16, 23 }; int sum = 0; for (int *p = a; p < a + 5; p++) sum = sum + *p; printf("sum = %d\n", sum); return 0;}main has started. a is five int boxes side by side from 0x7fd0, 4 bytes each; nothing has a value yet.
Output
Describe this picture
Across the top, a picture of the array a as it lies in memory: five boxes side by side, each as wide as its bytes, with its index, 0 to 4, above it and its address below it. For int the boxes start at 0x7fd0 and go up by 4: 0x7fd0, d4, d8, dc and e0. Under the strip is its declaration and size, “int a[5] · 20 bytes”, and under that two smaller boxes, sum and p, each with its type, name and address. When the loop starts, a tag named p appears over the box whose address p holds, carrying that address. Under the picture are the 11 lines of walk.c with the line about to run marked, one sentence on the current step beside them, then the switch and the program’s output. The clip plays the program once, 16 steps in about 16 seconds, and rests on the end. Line 5 fills the boxes with 4, 8, 15, 16 and 23, and line 6 sets sum to 0. Then the loop runs five times: each time the tag hops one box to the right while the address it carries rises by 4, from 0x7fd0 to 0x7fe0, and the box under it is added into sum, which becomes 4, 12, 27, 43 and 66. After the fifth pass the tag hops once more, onto a dashed box just past the last one, at 0x7fe4; the loop ends and the tag fades. sum = 66 appears in the output. The switch, “Element type”, with the positions int and double, swaps the program for walk-double.c: the boxes are drawn twice as wide, starting at 0x7fc0 and going up by 8, they hold 0.5, 1.5, 2.5, 3.5 and 4.5, the tag carries 0x7fc0, c8, d0, d8 and e0 and then e8, and the output is sum = 12.5. A press replays the walk from the start. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as walk.step, and the switch as walk.type.
One box after another
The five boxes of a sit one after another in memory, with no gaps: a[0] at 0x7fd0, a[1] 4 bytes later at 0x7fd4, and so on. That is the whole trick. To find a[3], the machine starts at 0x7fd0 and adds 3 × 4 = 12 bytes, which gives 0x7fdc. It never looks at a[0], a[1] or a[2] on the way.
Watch the tag above the strip. It is the pointer p, and the number it carries is the address in p. Each time the loop runs p++, the tag moves exactly one box, and the number goes up by 4, not by 1. Adding 1 to a pointer to int moves it to the next int.
Now press double. The boxes are 8 bytes wide, so the tag still moves one box per hop, and the address now goes up by 8. The step is always the size of one element.
At the end the tag lands on a dashed box past the last one, at 0x7fe4. That is a + 5. The loop compares p with it to know when to stop, and never reads the box there.
Key idea
An array is a row of equal boxes side by side. a[i] is at the array’s address plus i times the size of one box, and adding 1 to a pointer moves it on by exactly that size.
The code and its cost
Declaring and indexing
int a[5] = { 4, 8, 15, 16, 23 };
declares a as five int boxes in a row, 20 bytes, and fills them in order. The boxes are numbered from 0: a[0] is 4 and a[4] is 23. There is no a[5]; the five indices are 0 to 4.
a[i] reads or writes box i, like any variable: a[2] = 7; writes 7 into the third box.
a + i and *(a + i)
Used in an expression, the name a stands for the address of a[0], 0x7fd0 here. Adding a number to an address works in elements, not bytes: a + i is the address i boxes further on, i * sizeof a[0] bytes past a. For int, a + 2 is 0x7fd0 + 8 = 0x7fd8.
And a[i] means exactly *(a + i): go i boxes along from a, then read the box there. The square brackets are a shorter way to write the same thing. That is why indices start at 0: a[0] is *(a + 0), the box at a itself.
The walk
The program in the picture adds up the array by walking a pointer along it:
for (int *p = a; p < a + 5; p++)
sum = sum + *p;
p starts at a, the address of a[0]. Each pass adds *p, the box p points to, then p++ moves p to the next box. After a[4], p becomes a + 5 and the test p < a + 5 fails. Line 8 runs five times, once for each box.
One past the end
C lets a pointer hold the address just past the last box, a + 5, so that a loop can compare against it. It does not let you follow it: there is no a[5], and *(a + 5) is undefined behaviour. On this machine, with this build, it reads whatever happens to lie next in memory. The picture draws that box dashed for this reason.
An array is not a pointer
a behaves like an address in an expression, but a itself is the 20 bytes of boxes, not a pointer box. Two things show it. sizeof a is 20, the whole array, where a pointer is 8. And a = a + 1; does not compile: an array cannot be moved. A pointer p can.
Passing an array to a function
Here is the sum written as a function, sum(const int *a, size_t n), and a main that calls it on the same five numbers. Watch what the call hands over.
Passing an array to a function
sum gets the address of a[0], not the five ints
#include <stddef.h>#include <stdio.h> int sum(const int *a, size_t n){ int total = 0; for (size_t i = 0; i < n; i++) total = total + a[i]; return total;} int main(void){ int a[5] = { 4, 8, 15, 16, 23 }; printf("sum = %d\n", sum(a, 5)); printf("sizeof a in main = %zu\n", sizeof a); return 0;}Output
main has started. a is five int boxes side by side from 0x7fd0, 20 bytes; nothing has a value yet.
Describe this picture
The 18 lines of sum-function.c with the line about to run marked, beside a picture of memory under the label “Stack”. At the top is main’s card: its array a, five int boxes from 0x7fd0, drawn in rows of 8 bytes with higher addresses on top: a[0] at 0x7fd0 and a[1] at 0x7fd4 in the bottom row, a[2] at 0x7fd8 and a[3] at 0x7fdc in the middle row, and a[4] at 0x7fe0 alone in the top row. Below it, while sum runs, is sum’s card: a, a const int * of 8 bytes at 0x7fb8; n, a size_t of 8 bytes at 0x7fb0; total, an int at 0x7fac; and i, a size_t at 0x7fa0 while the loop runs. Each box shows its type and name, its address and its value, ”?” until a line writes one. Under the picture the program’s output appears once it prints, and under everything one sentence on the current step. The replay plays once, 19 steps in about 19 seconds, and rests on the end. Line 14 fills main’s boxes with 4, 8, 15, 16 and 23. At the call, sum’s card fades in: 0x7fd0 flies from a[0]‘s corner into sum’s a and grows into an arrow from sum’s a up to main’s a[0], and n holds 5. The loop runs five times; each pass, i takes the next index and total becomes 4, 12, 27, 43 and 66 as each of main’s boxes is read. Then sum’s card fades out, the arrow with it, and the output shows sum = 66, then sizeof a in main = 20. The box the last line wrote is outlined. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as fn.step.
The program prints sum = 66, then sizeof a in main = 20. In the call sum(a, 5), a stands for the address of a[0], so what the function gets is one 8-byte pointer holding 0x7fd0, not the 20 bytes of boxes. Its own a is that pointer, and a[i] inside sum reads main’s boxes through it: watch total grow while main’s five boxes stay exactly as they were. Writing the parameter as int a[] changes nothing: it is still a pointer.
So inside sum, sizeof a is 8, the size of a pointer, and the function has no way to find out how many boxes there are. That is why the length travels beside the array, as n. The const from Passing pointers to functions (2.2) says sum only reads them.
No bounds checks
C does not check an index. With int a[5], a[5] compiles, but it is undefined behaviour. On this machine, with this build, it reads the 4 bytes after a[4], whatever they hold, and a[5] = 0; overwrites them. Memory errors (2.5) shows what that does and how to catch it.
What it costs
Finding a[i] is one multiplication and one addition, i × 4 added to the address of a, whatever i is: a[4] costs no more than a[0], and in an array of 5,000 boxes neither would a[4999]. Walking the whole array is one step per box, so n boxes take n steps.
Worked example
Take int a[5] = { 4, 8, 15, 16, 23 }; at 0x7fd0, as in the picture.
&a[3]is 0x7fd0 + 3 × 4 = 0x7fd0 + 12 = 0x7fdc.*(a + 2)is the box 2 along from a[0]: 15.- After
int *q = a + 1;, q points at a[1]. Thenq[2]is*(q + 2), two boxes past a[1]: that is a[3], 16. - For the
doublearray at 0x7fc0, each box is 8 bytes, so&a[3]is 0x7fc0 + 3 × 8 = 0x7fc0 + 24 = 0x7fd8. - An image of 640 × 480 pixels, one byte each, stored row after row in one array: the pixel in row 2, column 10 is element 2 × 640 + 10 = 1290.
Try it yourself. If p holds 0x7fd8 and points into the int array, what does p + 2 hold? Two ints on, 8 bytes: 0x7fe0, the address of a[4]. And what is &a[5]? 0x7fe4, an address you may compute and compare, never follow.
Where it’s used
Audio and sensor buffers. A sound or a sensor’s readings are stored as a long array of samples. Convolution, the heart of digital filtering, is a loop that walks such arrays with exactly these pointer steps.
Images. A picture is an array of pixels, row after row, and finding pixel (row, column) is the arithmetic of the worked example.
Tables. Chapter 5 builds searching, inserting and growing on arrays like this one.
Strings. A string in C is an array of char, one byte per box, which Chapter 6 takes apart.
Reference card
| Code or idea | Meaning |
|---|---|
int a[5] = { 4, 8, 15, 16, 23 }; | five int boxes side by side, 20 bytes, indices 0 to 4 |
a in an expression | the address of a[0] |
a + i | the address i boxes on: i * sizeof a[0] bytes past a |
a[i] | *(a + i): the box i along from a[0] |
p++ | moves p to the next box, by the size of one element |
a + 5 | one past the end: may be compared, never followed |
sizeof a | 20 in main; 8 inside a function, where a is a pointer |
f(const int *a, size_t n) | an array arrives as a pointer; pass its length beside it |
| bounds | C checks no index: a[5] is undefined behaviour (here, it reads whatever lies next) |
| cost | finding a[i] is one multiply and one add, whatever i is |
Platform: C11, gcc 13 on x86-64 Linux: int 4 bytes, double 8, pointers 8. The addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.