6. Arrays, pointers, and the arithmetic that explains everything else
6.1 An array is a contiguous block; a pointer is not an array
#include <stdio.h>
int main(void) {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // arrays decay to a pointer to their first element
printf("%d %d\n", arr[2], *(p + 2)); // identical: 30 30
printf("%zu\n", sizeof(arr)); // 20 — sizeof knows arr is 5 ints
printf("%zu\n", sizeof(p)); // 8 — sizeof(p) is just a pointer's size
return 0;
}
This is the single most important fact in C: arr[i] is defined as
*(arr + i). Pointer arithmetic is scaled by the pointee's type — p + 1
moves forward sizeof(*p) bytes, not 1 byte. This is why void*
arithmetic isn't standard C (the compiler doesn't know the scale) and why
sizeof on a decayed pointer is a classic bug: the moment an array is
passed to a function, it decays to a pointer and sizeof inside that
function returns the pointer's size (8), not the array's.
void bad_len(int arr[]) {
printf("%zu\n", sizeof(arr)); // 8, NOT the caller's array size — classic beginner bug
}
6.2 Pointer arithmetic, walked through
#include <stdio.h>
int main(void) {
int nums[] = {1, 2, 3, 4, 5};
int *begin = nums;
int *end = nums + 5; // one past the last element — legal to compute, not to dereference
for (int *p = begin; p != end; p++) {
printf("%d ", *p);
}
printf("\n");
ptrdiff_t count = end - begin; // 5 — subtracting pointers gives element count
printf("%td\n", count);
return 0;
}
This begin/end idiom is exactly how C++ iterators and Go slices are
modeled under the hood — C just makes the pointer arithmetic explicit.
6.3 const correctness with pointers — read right to left
int x = 5;
const int *p1 = &x; // pointer to const int: *p1 = 6 is an error, p1 = &y is fine
int *const p2 = &x; // const pointer to int: *p2 = 6 is fine, p2 = &y is an error
const int *const p3 = &x; // const pointer to const int: neither is fine
Real-world example. Function signatures use const to document (and
have the compiler enforce) "this function reads but does not modify" —
this is why almost every standard-library string function that only reads
takes const char *:
size_t my_strlen(const char *s) { // promise: won't write through s
size_t n = 0;
while (s[n] != '\0') n++;
return n;
}
🔎 Checkpoint: pointers and arrays
Answer these before moving on — this section is the one most beginners need to revisit, and the fastest way to know if you actually need to is to try these first.
1. Predict the output, then check yourself:
#include <stdio.h>
int main(void) {
int arr[4] = {10, 20, 30, 40};
int *p = arr + 1;
printf("%d %d\n", *p, p[1]);
return 0;
}
Answer
20 30. p points at arr[1] (value 20), so *p is 20. p[1] is
defined as *(p + 1), and p is already arr + 1, so p[1] reaches
arr[2], which is 30.
2. Spot the bug:
void print_len(int arr[]) {
printf("%zu\n", sizeof(arr) / sizeof(arr[0]));
}
Answer
arr decayed to int * the moment it crossed the function boundary
(§6.1), so sizeof(arr) is the pointer's size (8 on a 64-bit machine),
not the caller's array size — this division computes 8 / 4 = 2,
always, no matter how big the array actually was. There is no fix that
keeps this signature; the only correct fix is to pass the length in
separately: void print_len(int *arr, size_t len). This exact mistake
is real enough that Clang/GCC warn about it by default (§46.8).
3. Write it yourself: write int sum_array(const int *arr, size_t len)
that returns the sum of len elements, then call it correctly on a
stack-allocated array.
One correct answer
#include <stdio.h>
int sum_array(const int *arr, size_t len) {
int total = 0;
for (size_t i = 0; i < len; i++) total += arr[i];
return total;
}
int main(void) {
int nums[] = {1, 2, 3, 4, 5};
printf("%d\n", sum_array(nums, sizeof(nums) / sizeof(nums[0]))); // 15
return 0;
}
Note sizeof(nums) / sizeof(nums[0]) at the call site — this only
works here because nums hasn't decayed yet; it's a real array in the
scope where sizeof sees it. Try moving that computation into a helper
function that takes int nums[] as a parameter and predict what breaks.
4. Conceptual — why does this compile, and what does it print?
#include <stdio.h>
int main(void) {
int arr[4] = {10, 20, 30, 40};
printf("%d\n", 2[arr]); // yes, this is legal C — see the answer
return 0;
}
Answer
It prints 30, identically to arr[2]. arr[2] is defined as
*(arr + 2); addition is commutative, so *(arr + 2) and *(2 + arr)
are the same expression, and 2[arr] means exactly *(2 + arr). This
isn't a special case in the grammar — a[b] is *(a + b) for any
pointer/integer pair, in either order. It's a well-known trivia question
precisely because it exposes how literally the array-indexing rule from
§6.1 should be taken.