Why this matters
C arrays are much closer to raw memory than arrays in most languages: int scores[5] reserves five int-sized
boxes laid out contiguously, and that’s it – no length stored alongside them, no bounds checking, nothing else.
You already know what an address is and what a pointer does with one, from the last two lessons; this lesson’s one
big idea is that scores[i] is really just pointer arithmetic in disguise, not a separate array feature. Once
that clicks, malloc’d arrays (next lesson) and eventually C-style strings all turn out to be the same idea
applied in different places, instead of separate things to memorize.
scores[i] and *(scores + i) are the same expression
This is not an analogy – the compiler treats a[i] as literally shorthand for *(a + i). When you write
scores[2], the compiler computes the address scores + 2 (which, thanks to pointer arithmetic below, correctly
points at the third element) and dereferences it. The worked example prints both forms for the same element to
make this concrete: they produce identical results because they are identical, just spelled differently.
Pointer arithmetic is scaled by the pointee’s size
scores + 2 does not mean “2 bytes past scores.” It means “2 ints past scores” – the compiler multiplies
the 2 by sizeof(int) automatically, because it knows scores is an int*. This is exactly why array indexing
works at all: a[i] finds the i-th element regardless of whether each element is 1 byte or 8, because the
pointer type carries that information. It’s also why you cannot do pointer arithmetic on a void* – there’s
nothing to scale by.
An array “decays” to a pointer almost everywhere you use it
Write for (int *p = scores; ...) and scores – an array – gets converted to a pointer to its first element.
This happens automatically in most expressions (this is called “array decay”), which is why scores + 5 in the
worked example’s loop condition is legal at all: you’re doing pointer arithmetic on int*, not on the array
itself. The loop walks a pointer from scores (element 0) up to, but not including, scores + 5 (one past the
last element) – a very common C idiom for “iterate the whole array” that you’ll see constantly.
One place decay does not happen: sizeof. sizeof(scores) inside main correctly gives 20 (5 ints × 4
bytes) because scores is still a real array there. But if you pass scores to a function, the parameter receives
a decayed pointer, and sizeof inside that function would give you the pointer’s size (8, typically), not the
array’s – a well-known trap. That’s why array-processing functions in C almost always take an explicit length
parameter alongside the pointer, which is the convention every exercise below follows.
There is no bounds checking
scores[10] for a 5-element array compiles and runs – it computes an address 10 ints past scores and reads
or writes whatever happens to be there. This is undefined behavior, not a caught error, and it’s one of C’s
sharpest edges. Every exercise below gives you the array’s length explicitly for exactly this reason: it’s your
job to stay inside [0, length), because nothing else will.
Two-dimensional arrays are one block of memory, indexed with a formula
int grid[3][4]; // 3 rows, 4 columns -- 12 ints total
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 4; col++) {
grid[row][col] = row * 4 + col;
}
}
int grid[3][4] doesn’t create three separate arrays – it reserves one contiguous block of 3 * 4 = 12 ints,
laid out row by row (all of row 0, then all of row 1, then all of row 2). grid[row][col] is really computing a
single offset into that block: conceptually, *(grid + row * 4 + col) – “skip row whole rows (4 ints each),
then col more” – though the compiler works out the exact arithmetic for you from the declared dimensions. You
don’t need to compute that offset by hand; what’s worth internalizing is that it’s still just one flat region of
memory underneath, accessed with two indices instead of one, not some fundamentally different kind of array.
The nested loop above visits every element exactly once, row by row – the standard pattern for processing a 2D
array, and the one every 2D exercise below uses. Get comfortable naming your two loop variables clearly (row and
col, not two is) – nested loops over a grid are exactly where reused, ambiguous loop-variable names cause real
confusion about which index means what.