array indexing as pointer arithmetic
The square-bracket subscript a[i] feels like a built-in array feature, but in C it is pure sugar for pointer arithmetic. By definition, a[i] means exactly the same as *(a + i): take the pointer, step forward i elements, and dereference. Indexing is not a separate mechanism — it is two things you already know dressed up to read nicely.
Follow the steps for a[2] where a is an int *: first a + 2 advances by two elements, which is 2 * sizeof(int) = 8 bytes, to the address of the third int; then * dereferences that address to read or write the value there. Because a[i] is literally defined as *(a + i), and addition commutes, the strange-looking i[a] is also legal and means the same thing — a quirky proof that the subscript is just arithmetic. This also explains how indexing a decayed array works: once int a[10] becomes a pointer, a[i] is just pointer arithmetic on that pointer.
Two honest consequences follow. First, there is no automatic bounds checking — a[i] computes an address and dereferences it whether or not i is in range, so a[100] on a 10-element array is an out-of-bounds access and undefined behaviour, the root of countless buffer overflows. Second, indexing and pointers are genuinely interchangeable: p[i], *(p + i), and walking a pointer with p++ are three ways to express the same traversal, and which you pick is a matter of clarity, not capability.
int a[3] = {5, 6, 7}; — then a[1], *(a + 1), and even 1[a] all evaluate to 6. The compiler turns a[1] into *(a + 1): one element (4 bytes) past the start, then dereferenced.
a[i] is defined as *(a + i) — subscripting is sugar over arithmetic.
Because a[i] is just *(a + i), the subscript never checks that i is valid. An index past the end (or negative) silently computes a bad address and reads or writes there — undefined behaviour, not a guaranteed crash. Keeping indices in range is entirely the programmer's responsibility in C.