Arrays and Pointer Arithmetic
How arrays really work in C, array decay, pointer arithmetic, and passing arrays to functions
C arrays are the most direct expression of "contiguous bytes in memory with a name." They are also surprisingly subtle: an array is not quite a pointer, but it becomes one almost any time you use it.
A C array is contiguous memory
int nums[5] = {10, 20, 30, 40, 50}; reserves enough room for five
ints, side by side, and names the start nums.
The addresses should grow by exactly sizeof(int) per element (4 on
WASI). That is what we mean by "contiguous": no gaps, no indirection,
no per-element header.
a[i] is *(a + i)
Indexing in C is sugar for pointer arithmetic. The expression a[i]
is defined as *(a + i). The + i part is typed — it advances by
i * sizeof(*a) bytes, not by i bytes.
Yes, 2[a] is valid C: by definition i[a] == *(i + a) == *(a + i) == a[i]. Do not write that in real code — but knowing it explains
why arrays and pointers feel so interchangeable.
Array decay
Almost every time you use an array (pass it to a function, assign it to a pointer, use it in arithmetic), the expression of array type is implicitly converted to a pointer to its first element. This is called array-to-pointer decay.
Inside a function, int a[] is exactly the same as int *a. The
size information is lost. That is why C functions that take arrays
almost always also take a length parameter.
Always pass the length
There is no portable, automatic way to ask a T * "how many elements
do you point at?" — you have to track it yourself. This is the
single most important convention to internalize in C.
Pointer arithmetic in detail
If p is a pointer to a T, then:
| Expression | Meaning |
|---|---|
p + n | address of element n past p (n × sizeof(T) bytes ahead) |
p - n | address of element n before p |
p - q | number of elements between two pointers into the same array |
*p++ | dereference p, then advance p |
*++p | advance p, then dereference |
p[n] | shorthand for *(p + n) |
Pointers may only be compared within the same allocation
Subtracting or comparing pointers that point into different arrays
is undefined behavior. The "one-past-the-end" pointer (a + 5 for an
array of length 5) is a legal sentinel but must not be dereferenced.
Functions that take arrays
The classic signature is void f(T *arr, size_t n). Inside the
function, treat arr and n as inseparable.
The idiom sizeof(array) / sizeof(array[0]) works only where the
array's type is in scope — that is, where it has not yet decayed.
Hence we compute n in main and pass it down, never inside sum.
2D arrays vs arrays of pointers
Two ways to represent a matrix. They look similar; they are not the same in memory.
int grid[3][4]; // 12 ints in one contiguous block
int *rows[3]; // 3 pointers, each pointing at its own rowThe first is one allocation of 3 * 4 * sizeof(int) = 48 bytes laid
out in row-major order. The second is three separate allocations
(plus an array of three pointers) — flexible but with poorer cache
behavior.
Row-major layout means grid[r][c] lives at offset
(r * 4 + c) * sizeof(int) from grid. Iterating in row-major order
(outer loop over rows, inner loop over columns) walks memory linearly
and is cache-friendly.
Bounds checking — there isn't any
C arrays do not check bounds. Writing a[100] for a 10-element array
will quietly stomp on whatever memory follows the array — a heap
metadata structure, another variable, the return address on the
stack. That is the C bug.
On the WASI target you may see sentinel change to 42, stay at
999, or get something else entirely. We will return to bugs like
this in the Memory Bugs chapter.
Practice: rotate an array left by one
Implement void rotate_left(int *arr, size_t n) so that the element at index 0 moves to the end and every other element shifts one position to the left. For {1,2,3,4,5} the result is {2,3,4,5,1}. The provided main prints the array space-separated on one line.
Practice: matrix diagonal sum
Implement int diag_sum(int m[4][4]) that returns m[0][0] + m[1][1] + m[2][2] + m[3][3]. The provided main defines a 4x4 matrix and prints the result followed by a newline.
Test your understanding
Inside void f(int a[], size_t n), what does sizeof(a) give you?
The total number of bytes in the original array.
The size of a pointer (because a has decayed to int *).
The number of elements in the array.
Always zero.
Given int a[10]; and int *p = a + 3;, what is p - a?
3 bytes
3 * sizeof(int)
3 elements
Undefined behavior
What happens if you write past the end of a C array, e.g. a[100] = 0 for an array of length 10?
The compiler refuses to build the program.
The runtime throws an ArrayIndexOutOfBoundsException.
The write silently fails and leaves memory unchanged.
It is undefined behavior — it may crash, may overwrite unrelated data, or may appear to work.