Pointers & Arrays

Pointers are C's superpower and its curse: a pointer stores the address of another object, letting you share, move, and reshape data without copying. Every systems language copied the idea — Zig's slices, Rust's references, Go's pointers — but none of them run as close to the metal. Understand this page and you understand C.

Addresses & Dereferencing

The address-of operator & produces a pointer to an object; the dereference operator * goes back from the pointer to the object. The type of &x is int * ("pointer to int"): it records both the address and the type of the thing it points to. A pointer that points nowhere is NULL — the one value you may compare with pointers safely, and the one you must check before dereferencing.

#include <stdio.h>

int main(void) {
    int  x = 42;          // a plain int object on the stack
    int *p = &x;          // p holds the address of x; type: pointer to int

    printf("x's value:  %d\n", x);    // 42
    printf("p's address:%p\n", (void *)p);  // some memory address
    printf("deref *p:   %d\n", *p);   // 42 — *p means "the int at p"

    *p = 99;              // write THROUGH the pointer: x is now 99
    printf("x is now:   %d\n", x);    // 99 — modified via p
    return 0;
}
Pointer holding an address, dereferenced to reach the value

Figure 1 — p stores the address of x; *p reads the value stored at that address.

Dereferencing a NULL or an invalid pointer is undefined behavior — on most systems a crash, on some a security hole. The discipline: initialize pointers to NULL or a real address, check before use, and never dereference memory you do not own.

Pointer Arithmetic

Adding an integer to a pointer steps by the size of the pointee, not by one byte: p + 1 on an int * advances sizeof(int) bytes to the next int. This is how arrays and pointers unify — the whole array model of C is built on it.

#include <stdio.h>

int main(void) {
    int arr[4] = {10, 20, 30, 40};
    int *p = arr;                 // array decays to pointer to first element
    printf("first: %d\n", *p);    // 10  — p points at arr[0]
    p++;                          // advance to the next int (4 bytes)
    printf("second:%d\n", *p);    // 20
    p += 2;                       // two ints further
    printf("fourth:%d\n", *p);    // 40
    // the pleasant surprise: p[i] is exactly *(p + i)
    printf("arr[2] via ptr: %d\n", *(arr + 2));   // 30
    return 0;
}

Pointer subtraction works too: p - arr yields the index distance between two pointers into the same array. Do not compare pointers from different arrays — that comparison is undefined.

Arrays Decay Into Pointers

An array name used as a value decays into a pointer to its first element. That is why arr and &arr[0] are the same pointer value — but they are not the same type as arr itself (which is "array of 4 int", a different beast that keeps its size). The famous consequence: inside a function parameter, int a[] is actually int *a, and sizeof(a) inside that function is the pointer size, not the array size. Always pass the length alongside the array.

#include <stdio.h>

// the parameter syntax "int a[]" is sugar for "int *a" — a decays
int total(int a[], int n) {     // 'n' MUST travel with the array
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += a[i];            // a[i] is *(a + i) — pointer math again
    }
    return sum;
}

int main(void) {
    int nums[] = {2, 4, 6, 8};
    printf("total = %d\n", total(nums, 4));   // 20
    return 0;
}

Pointer to Pointer

A int ** is a pointer that stores the address of another pointer. Two uses matter daily: mutating a caller's pointer from inside a function ("out parameter"), and building matrices as arrays of row pointers. The classic example is a function that must change which object the caller's pointer refers to — reach the caller's pointer through ** and assign through it.

#include <stdio.h>

// assign a = b by writing through a pointer-to-pointer
void swap_pointers(int **pa, int **pb) {
    int *tmp = *pa;   // deref once: the pointer a
    *pa = *pb;        // write through: now pa points to b's target
    *pb = tmp;
}

int main(void) {
    int x = 1, y = 2;
    int *a = &x, *b = &y;
    swap_pointers(&a, &b);     // pass the ADDRESSES of the pointers
    printf("a now points to: %d\n", *a);   // 2 (was x, now y)
    printf("b now points to: %d\n", *b);   // 1
    return 0;
}

Void Pointers

void * is the "generic pointer": it can hold any address without committing to a pointee type. The standard library's memory functions use it — malloc returns void * precisely because it does not know what you will store. You must cast it back before dereferencing or doing arithmetic, because arithmetic needs a pointee size.

#include <stdlib.h>   // malloc, free
#include <stdio.h>

int main(void) {
    void *raw = malloc(10 * sizeof(int));   // 10 ints, untyped handle
    if (raw == NULL) {                      // always check allocation!
        return 1;
    }
    int *nums = (int *)raw;                 // cast to the intended type
    nums[0] = 7;                            // now arithmetic works: 4-byte steps
    printf("first value: %d\n", nums[0]);
    free(nums);                             // free the allocation, not the cast
    return 0;
}

Pointers to Struct

Passing a struct by pointer avoids copying its bytes on every call and lets the callee modify the original. The arrow -> is shorthand for dereference-then-dot: p->field means (*p).field. This pattern is the foundation of every C data structure and the collections page.

#include <stdio.h>

struct Point { int x, y; };

// move the point in place — no copy, caller's struct is updated
void translate(struct Point *p, int dx, int dy) {
    p->x += dx;      // (*p).x += dx
    p->y += dy;
}

int main(void) {
    struct Point home = {0, 0};
    translate(&home, 3, 4);       // pass address, mutate original
    printf("home = (%d, %d)\n", home.x, home.y);   // (3, 4)
    return 0;
}

Dangling Pointers — Return by Value Only

Never return the address of a local variable: its storage is reclaimed when the function returns, leaving a dangling pointer — reading through it is undefined behavior. Return the value, or allocate on the heap and transfer ownership (the memory page covers this properly).

int *bad(void) {
    int local = 5;
    return &local;   // BUG: local dies here; the caller gets a dead address
}

Compilers warn about this with -Wall; sanitizers catch it at runtime. Both are why we enabled them on the setup page.

Const-Correct Pointers

The position of const changes the promise. Read right-to-left from the star to decode it:

DeclarationMeaning
const int *ppointer to const int — may not write through p
int *const pconst pointer to int — may not change p itself
const int *const pboth: read-only object through a fixed pointer

Declaring function parameters as const int * tells callers you will not mutate their data — and lets the compiler enforce that promise.

Next: functions, scope, and linkage — how C organizes executable code.