Functions, Arrays & Pointers

Arrays & Pointer Arithmetic

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.

Try it
Output will appear here.

Exercises