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.
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
#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
#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).
#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.
#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.
* 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.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(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.
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.