Pointers and Arrays
When an array is used in most expressions, it 'decays' into a pointer to its first element — that's why int *p = numbers works with no & needed. Once p points at the array's start, p + 1 doesn't mean 'add 1 to the address' — it means 'move forward by one int', with the compiler automatically scaling by sizeof(int).
#include <stdio.h>
int main() {
int numbers[3] = {10, 20, 30};
int *p = numbers;
printf("%d %d %d\n", *p, *(p + 1), *(p + 2));
printf("%d %d %d\n", p[0], p[1], p[2]);
return 0;
}10 20 30 10 20 30
numbers[i] is literally defined as *(numbers + i) — square-bracket indexing is pointer arithmetic with friendlier syntax, not a separate language feature. This is why C arrays have no bounds checking: numbers[i] is just an address computation, and the compiler has no way to know if that address is still inside the array.
Next: strings — why C has no built-in string type, and what one actually is under the hood.
Practice exercise
Given int numbers[3] = {10, 20, 30} and int *p = numbers, print the second element using pointer arithmetic (*(p + 1)).