roadmap
week 3 · day 17

Arrays & Pointer Arithmetic

Why a[i] is literally *(a + i)

Medium 32 min 180 xp
After this you can
  • Explain that an array name decays to the address of its first element
  • Derive the identity a[i] == *(a + i), and why i[a] also compiles
  • Predict how pointer arithmetic scales by element size
  • Connect array indexing to the [base + index*scale] addressing mode from day 6

Arrays and pointers are two views of one thing

An array in C is a block of equal-sized elements laid out contiguously in memory — element 0, then element 1 right after it, and so on. The name of the array, used in an expression, decays to a pointer to its first element. So a (an int array) is, in most contexts, just &a[0] — an address. Arrays and pointers aren't the same thing, but array *indexing* is defined entirely in terms of pointer arithmetic, which is why they feel intertwined.

The identity that explains everything

Here is the definition of subscripting in C, and it's worth carving into your memory:

Key idea
**a[i] is defined as *(a + i).** The subscript brackets are pure syntactic sugar for "take the base address a, add i, dereference." That's the whole rule. And because a + i and i + a are the same, a[i] equals i[a] — a legal (if cursed) way to write it that proves indexing is just commutative pointer addition, not a special array operation.

But "add i" means "add i elements," not i bytes

The subtlety that makes it work: pointer arithmetic scales by the size of the pointee. a + 1 on an int* (4-byte ints) doesn't move 1 byte — it moves 4 bytes, to the next int. a + i moves i * sizeof(element) bytes. This is why the type on a pointer matters (day 16): the type is what the compiler multiplies by.

an int array in memory; a+i scales by 4
   a[0]    a[1]    a[2]    a[3]
  ┌──────┬──────┬──────┬──────┐
  │  10  │  20  │  30  │  40  │   each cell = 4 bytes (int)
  └──────┴──────┴──────┴──────┘
  0x1000 0x1004 0x1008 0x100c

  a        = 0x1000        (address of a[0])
  a + 2    = 0x1008        (NOT 0x1002 — scaled by 4)
  *(a + 2) = a[2] = 30

Look at that address: a + 2 is 0x1000 + 2*4 = 0x1008, exactly where a[2] lives. Now recall day 6's addressing mode: [base + index*scale], with scale = element size. a[i] compiles to [a + i*4] — the array's base, the index, times 4 (the int size). You already learned to read array indexing in assembly; this is the same thing in C. The C a[i], the pointer expression *(a+i), and the machine's [base + index*scale] are three spellings of one operation.

arr.cc
1int a[4] = {10, 20, 30, 40};
2int x = a[2]; // 30
3int y = *(a + 2); // 30, identical to a[2]
4int z = 2[a]; // 30 too — cursed but valid
5
6// walking with a pointer:
7for (int *p = a; p < a + 4; p++) {
8 // *p is a[0], a[1], a[2], a[3] in turn; p++ moves 4 bytes each step
9}

Why there's no bounds checking

C computes a + i and dereferences it — it does not check that i is within the array. a[7] on a 4-element array cheerfully reads *(a + 7), memory past the end of the array. There's no guard; the language trusts you. That's fast and dangerous in equal measure — reading or writing out of bounds is undefined behaviour (day 28) and the seed of countless bugs and exploits (recall the stack buffer from day 14: writing buf[i] with a too-large i walks straight toward the return address).

Predict first
long b[3]; starts at address 0x2000. long is 8 bytes. What address does b[2] (i.e. *(b+2)) read from? And what address would the out-of-bounds b[3] touch?
Note
A famous gotcha: an array name decays to a pointer, but it is *not* a modifiable pointer variable — you can't do a++ on an array, and sizeof(a) gives the whole array's byte size, while sizeof(p) on a pointer gives 8. Also, when you pass an array to a function it decays to a bare pointer, so the function can't know its length — you must pass the length separately. This is why C string/array functions always take a size.
finished reading?
Your task, you write the code

Prove the identity, then walk memory

In arrays.c: (1) declare an int array and verify a[i] == *(a+i) == i[a] for a couple of indices by printing all three. (2) Print &a[0], &a[1], &a[2] and confirm the addresses differ by exactly sizeof(int). (3) Walk the array two ways — by index a[i], and by a moving pointer p++ — and show they visit the same elements. Then compile at -O0 and find the [base + index*scale] operand for the indexing. Uses only this lesson plus day 6/16.

deliverable: arrays.c (+ the indexing disassembly line)
build & run
$ gcc -O0 -g -Wall arrays.c -o arrays && ./arrays
$ objdump -d -M intel arrays
self-review before running
  • You showed a[i], *(a+i), and i[a] give the same value
  • Consecutive element addresses differ by exactly sizeof(int)
  • You walked the array with a pointer (p++) and by index, matching results
  • You found the [base + index*scale] addressing mode in the disassembly
stretchChange int to long and observe the address stride become 8, and the scale in the disassembly become 8. Then deliberately read a[N] one past the end, run under `-fsanitize=address`, and read the out-of-bounds report — the sanitizer catching the exact thing C won't.

Self-check

01In C, a[i] is defined as:
02For an int array `a` (4-byte ints), `a + 2` moves how many bytes from a?
03How does array indexing relate to day 6's addressing modes?
04What does C do when you access a[7] on a 4-element array?
0/4 correct · 0/4 checked