thecodex.expert · The Codex Family of Knowledge
C

Pointers and Memory

C gives you the keys to every byte in memory. No guardrails, no garbage collector — just you, the address space, and the consequences.

C17 / C23 Manual memory management Last verified:
Canonical Definition

A pointer in C is a variable holding a memory address — int* p stores the address of an int; &x takes the address of x; *p dereferences to access the value at that address. malloc allocates heap memory and returns a pointer; free returns it to the OS. Every malloc must be paired with exactly one free.

The central idea

Every variable in your program lives at a specific address in memory. C lets you work with those addresses directly. A pointer is just a number — the address of something. The power: you can share access to data without copying it. The danger: nothing stops you from accessing the wrong address, corrupting memory, or forgetting to clean up.

Pointers: &, *, and basic usage

Cpointers.c
#include <stdio.h>

void increment(int *p) {   /* receives address of an int */
    *p = *p + 1;           /* dereference: read and write the value at the address */
}

int main(void) {
    int x = 10;
    int *p = &x;   /* p holds the address of x */

    printf("x = %d\n", x);       /* 10 */
    printf("&x = %p\n", &x);     /* address, e.g. 0x7ffee4b2c  */
    printf("p = %p\n", p);        /* same address */
    printf("*p = %d\n", *p);      /* 10 — value at that address */

    *p = 99;   /* write through the pointer */
    printf("x = %d\n", x);   /* 99 — x was changed via the pointer */

    increment(&x);
    printf("x = %d\n", x);   /* 100 */

    /* Pointer to pointer */
    int **pp = &p;   /* pp holds the address of p */
    printf("**pp = %d\n", **pp);   /* 100 */

    return 0;
}

Heap memory: malloc, calloc, realloc, free

Cheap.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    int n = 5;

    /* malloc: allocate uninitialized memory */
    int *arr = malloc(n * sizeof(int));
    if (arr == NULL) {   /* ALWAYS check — malloc can fail */
        fprintf(stderr, "malloc failed\n");
        return 1;
    }

    for (int i = 0; i < n; i++) arr[i] = i * i;
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");   /* 0 1 4 9 16 */

    /* realloc: resize — may move the allocation */
    int *bigger = realloc(arr, 10 * sizeof(int));
    if (bigger == NULL) { free(arr); return 1; }
    arr = bigger;   /* arr may now point somewhere else */

    /* calloc: allocate AND zero-initialise */
    double *matrix = calloc(4 * 4, sizeof(double));   /* 4x4 zeros */

    free(arr);       /* return heap memory — required */
    free(matrix);
    /* arr and matrix now point to freed memory — dangling pointers
       best practice: set to NULL after free */
    arr = NULL;
    matrix = NULL;

    return 0;
}

Pointer arithmetic

Adding an integer to a pointer advances it by that many elements of the pointed-to type. p + 1 on an int* advances by sizeof(int) bytes — not 1 byte. This is how array indexing works internally: arr[i] is exactly *(arr + i).

Cpointer_arithmetic.c
#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;   /* pointer to first element */

    printf("%d\n", *p);       /* 10 */
    printf("%d\n", *(p+1));   /* 20 — advance by sizeof(int) bytes */
    printf("%d\n", *(p+4));   /* 50 */

    /* arr[i] == *(arr + i) — they compile to identical code */
    printf("%d\n", arr[2]);       /* 30 */
    printf("%d\n", *(arr + 2));   /* 30 */

    /* Iterating with a pointer */
    int *end = arr + 5;   /* one past the last element */
    for (int *it = arr; it != end; it++) {
        printf("%d ", *it);
    }
    printf("\n");   /* 10 20 30 40 50 */

    /* Pointer difference: distance in elements */
    printf("%td elements\n", end - arr);   /* 5 */

    return 0;
}

Stack vs heap: when to use each

The stack is fast, automatic, and limited (typically 1–8 MB). Local variables live on the stack and are freed when the function returns — no malloc needed. The heap is large (limited by virtual address space), manually managed, and survives function returns. Use the heap when: (1) the size is not known at compile time, (2) the data must outlive the current function, or (3) the data is too large for the stack.

Cstack_vs_heap.c
#include <stdlib.h>
#include <stdio.h>

/* WRONG: returns pointer to stack variable — dangling pointer after return */
int *bad_return(void) {
    int local = 42;
    return &local;   /* local is freed when function returns */
    /* using the returned pointer is undefined behaviour */
}

/* CORRECT: heap allocation survives the function return */
int *good_return(int value) {
    int *p = malloc(sizeof(int));
    if (p) *p = value;
    return p;   /* caller is responsible for calling free(p) */
}

/* CORRECT: pass in a pointer to caller-owned memory */
void fill(int *result, int value) {
    *result = value;   /* write to caller's variable — no heap needed */
}

int main(void) {
    /* Stack: fine for small, bounded, local data */
    int stack_arr[100];   /* 400 bytes on stack — fine */

    /* Heap: for large or dynamically sized data */
    int *heap_arr = malloc(1000000 * sizeof(int));   /* 4MB — use heap */
    free(heap_arr);

    int *p = good_return(99);
    printf("%d\n", *p);   /* 99 */
    free(p);   /* caller frees */

    return 0;
}

NULL pointers and defensive programming

A null pointer (NULL, defined as (void*)0 in C) is a pointer that holds address zero — a sentinel meaning "this pointer points to nothing." Dereferencing a null pointer is undefined behaviour — on most systems it causes a segmentation fault. Always check if malloc returns NULL before using the pointer. Always check if a pointer parameter is NULL before dereferencing.

Commonly confused
int* p, q declares p as int* and q as plain int. The * in a declaration binds to the variable name, not the type. int* p, q is the same as int *p, q — p is a pointer to int, q is just an int. To declare two pointers: int *p, *q. This is a common C gotcha and why many style guides put the * next to the variable name.
An array name is not a pointer — it decays to one. int arr[5] is an array; it has fixed size and address. arr in an expression decays to a pointer to its first element (&arr[0]). You cannot assign to an array name (arr = p is illegal), but you can assign to a pointer. sizeof(arr) gives the total array size; sizeof(pointer) gives the pointer's size (4 or 8 bytes) — not the array's size.
free does not zero the pointer. After free(p), the variable p still holds the old address — it's now a dangling pointer. Accessing it is undefined behaviour. The idiom: set it to NULL immediately after free — free(p); p = NULL;. Then any accidental dereference will segfault immediately and obviously rather than silently corrupting memory.

The C abstract machine and the memory model

The C standard defines behaviour in terms of an abstract machine — an idealised execution model that the compiler must faithfully implement for defined operations. The abstract machine has: automatic storage (stack), static storage (globals, static locals), thread storage (_Thread_local), and allocated storage (heap via malloc). The standard does not mandate a stack — it mandates that automatic variables have the correct lifetime. Compilers are free to implement this however they wish as long as the observable behaviour matches. This is why return-value optimisation (RVO) is valid: the compiler can elide copies that would be unobservable.

Allocator internals: dlmalloc, tcmalloc, jemalloc

malloc is a library function, not a system call. Internally, malloc calls brk/sbrk (extend the heap) or mmap (map anonymous pages) from the OS, then sub-allocates from those pages in a free list. The default allocator in glibc is ptmalloc2 (a derivative of Doug Lea's dlmalloc). For high-concurrency server applications, alternatives like jemalloc (used by Firefox, FreeBSD) and tcmalloc (Google) provide thread-local caches to reduce lock contention. The allocator's metadata (block headers, free lists) live adjacent to user data — a heap buffer overflow can corrupt allocator metadata, enabling heap exploitation.

Specification reference

ISO/IEC 9899:2018 (C17). iso.org. ISO/IEC 9899:2024 (C23). Sections 6.5.3 (address-of operator), 6.5.6 (pointer arithmetic), 7.22.3 (memory management). Kernighan, B. W. & Ritchie, D. M. (1988). The C Programming Language (2nd ed.). Prentice Hall. cppreference.com/w/c/memory.

Sources

1
ISO/IEC 9899:2018. C17 Standard. iso.org. — Authoritative C language specification.
2
Kernighan, B. W. & Ritchie, D. M. (1988). The C Programming Language (2nd ed.). Prentice Hall. — The original C reference.
3
cppreference.com. C memory management. en.cppreference.com/w/c/memory.
4
Seacord, R. C. (2013). Secure Coding in C and C++ (2nd ed.). Addison-Wesley. — Memory safety and common vulnerabilities.
5
Chen, S. et al. (2007). Understanding the Linux Virtual Memory Manager. Gorman. — Memory allocation internals.
Source confidence: High Last verified: Primary source: ISO/IEC 9899:2018 — C17 standard