Frontend Labs
⋯

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.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36