Arrays and pointers are deeply linked in C — an array's name, in most expressions, decays into a pointer to its first element.
int nums[3] = {10, 20, 30};
int *p = nums; // no & needed — nums already decays to &nums[0]
printf("%d\n", *p); // 10 — same as nums[0]
printf("%d\n", *(p + 1)); // 20 — same as nums[1]
printf("%d\n", p[2]); // 30 — pointers support [] syntax too
p++; // now points to nums[1]
printf("%d\n", *p); // 20
Pointer arithmetic is scaled by the pointee's type automatically: p + 1 doesn't add 1 byte, it advances by sizeof(int) bytes so it correctly lands on the next int, wherever that type's size happens to be. This is why p[2] and *(p + 2) are genuinely interchangeable — array indexing is defined in the language as syntactic sugar for pointer arithmetic.
When passing an array to a function, only the pointer to its first element is actually passed (recall the earlier "array decay" gotcha), so the function receives no information about the array's length on its own — it's standard practice to pass the length as a separate parameter alongside the pointer.
void printArray(const int *arr, int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
}
Marking the parameter const int *arr promises callers that printArray will only read through the pointer, never write through it — the compiler enforces this promise.