Pointers & Memory#

Pointers are the most powerful and most dangerous feature of C. A pointer is a variable that holds a memory address. Understanding pointers is understanding how the computer actually manages data.


What Is a Pointer?#

int x = 42;
int *p = &x;   // p holds the address of x

printf("%d\n", x);    // 42 (the value)
printf("%p\n", (void*)p);  // 0x7ffd5e8c3a4c (the address)
printf("%d\n", *p);   // 42 (dereference — read value at address)

*p = 100;              // write through the pointer
printf("%d\n", x);    // 100 (x was modified via p)
OperationSyntaxMeaning
Declare pointerint *pp is a pointer to int
Address-of&xGet the address of x
Dereference*pAccess the value at address p
NULL pointerNULL or (void*)0Points to nothing

Stack vs Heap#

┌─────────────────────┐ High address
│       Stack         │ ← function locals, grows downward
│  (automatic, LIFO)  │
├─────────────────────┤
│         ↓           │
│    (free space)     │
│         ↑           │
├─────────────────────┤
│       Heap          │ ← malloc'd memory, grows upward
│  (dynamic, manual)  │
├─────────────────────┤
│   Data (BSS + init) │ ← globals, statics
├─────────────────────┤
│       Text          │ ← program code (read-only)
└─────────────────────┘ Low address
Memory RegionAllocated ByFreed ByLifetime
StackFunction callFunction returnAutomatic
Heapmalloc(), calloc()free()Manual
Data (global)Program startProgram endProgram lifetime
Text (code)Program loadProgram endProgram lifetime

Dynamic Memory#

#include <stdlib.h>

// Allocate
int *arr = malloc(10 * sizeof(int));   // 10 ints, UNINITIALIZED
int *arr = calloc(10, sizeof(int));    // 10 ints, ZEROED

// Check for failure
if (arr == NULL) {
    perror("malloc failed");
    exit(1);
}

// Use
arr[0] = 42;
arr[9] = 100;

// Resize
arr = realloc(arr, 20 * sizeof(int)); // grow to 20 ints

// Free
free(arr);
arr = NULL;  // prevent use-after-free

The Four Memory Bugs#

BugCauseConsequence
Memory leakmalloc without freeUnbounded memory growth
Use-after-freeDereference after freeUndefined behaviour (crash, corruption)
Double freefree the same pointer twiceHeap corruption
Buffer overflowWrite past allocated boundsStack smashing, code execution
// LEAK — forgot to free
void leak(void) {
    int *p = malloc(100);
    return;  // p is lost, 100 bytes leaked
}

// USE-AFTER-FREE
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);  // UNDEFINED BEHAVIOUR

// DOUBLE FREE
free(p);
free(p);  // UNDEFINED BEHAVIOUR

// BUFFER OVERFLOW
int arr[5];
arr[10] = 42;  // writes beyond array bounds — UNDEFINED BEHAVIOUR

Pointer Arithmetic#

Pointers can be incremented and decremented. The step size is the size of the pointed-to type:

int arr[] = {10, 20, 30, 40, 50};
int *p = arr;    // points to arr[0]

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20 (next int = +4 bytes)
printf("%d\n", *(p + 2)); // 30

p++;
printf("%d\n", *p);       // 20

// arr[i] is syntactic sugar for *(arr + i)

Pointer Subtraction#

int *start = &arr[0];
int *end = &arr[4];
ptrdiff_t diff = end - start;  // 4 (elements, not bytes)

Arrays and Pointers#

Arrays and pointers are closely related but not identical:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;    // array decays to pointer to first element

// These are equivalent:
arr[2]      // 3
*(arr + 2)  // 3
p[2]        // 3
*(p + 2)    // 3

// But:
sizeof(arr)  // 20 (5 × 4 bytes — size of the array)
sizeof(p)    // 8 (size of a pointer on 64-bit)

Array decay: When you pass an array to a function, it decays to a pointer — the function receives a pointer, not a copy of the array. It cannot know the array’s length.

void print_array(int *arr, size_t len) {
    for (size_t i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
}

// ALWAYS pass the length separately
int data[] = {1, 2, 3};
print_array(data, 3);

Pointers to Pointers#

int x = 42;
int *p = &x;      // pointer to int
int **pp = &p;     // pointer to pointer to int

printf("%d\n", **pp);  // 42

// Used for:
// 1. Modifying a pointer in a function
void allocate(int **out) {
    *out = malloc(sizeof(int));
    **out = 42;
}
int *result;
allocate(&result);

// 2. Arrays of strings (char **)
char *names[] = {"Alice", "Bob", "Carol"};
// names is char**

Function Pointers#

// Declaration
int (*operation)(int, int);

// Assignment
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

operation = add;
printf("%d\n", operation(3, 4));  // 7

operation = sub;
printf("%d\n", operation(3, 4));  // -1

// As function parameter (callbacks)
void apply(int *arr, size_t len, int (*fn)(int)) {
    for (size_t i = 0; i < len; i++) {
        arr[i] = fn(arr[i]);
    }
}

int double_it(int x) { return x * 2; }
apply(data, 5, double_it);

// typedef for readability
typedef int (*BinaryOp)(int, int);
BinaryOp op = add;

Void Pointers#

void* is a generic pointer — it can point to any type but cannot be dereferenced directly:

void *generic = malloc(sizeof(int));
int *specific = (int *)generic;  // cast to use
*specific = 42;

// Used in generic APIs:
void qsort(void *base, size_t nmemb, size_t size,
            int (*compar)(const void *, const void *));

int compare_ints(const void *a, const void *b) {
    return (*(const int *)a) - (*(const int *)b);
}
qsort(arr, len, sizeof(int), compare_ints);

Common Idioms#

Null Check After Allocation#

int *data = malloc(n * sizeof(int));
if (!data) {
    fprintf(stderr, "out of memory\n");
    exit(EXIT_FAILURE);
}

Free and Nullify#

free(ptr);
ptr = NULL;  // prevents use-after-free (subsequent deref is a clean crash)

Sizeof with Variable, Not Type#

// GOOD — adapts if type changes
int *arr = malloc(n * sizeof(*arr));

// FRAGILE — type mismatch if arr changes to long*
int *arr = malloc(n * sizeof(int));

Key Takeaways#

  • A pointer is a variable holding a memory address. & gets the address, * dereferences it.
  • Stack memory is automatic (freed on function return). Heap memory is manual (malloc/free).
  • Every malloc needs a free. Every free should nullify the pointer. Every allocation should be null-checked.
  • Pointer arithmetic steps by the size of the pointed-to type, not by bytes.
  • Arrays decay to pointers when passed to functions — always pass the length separately.
  • Function pointers enable callbacks. void* enables generic programming (at the cost of type safety).
  • Buffer overflows, use-after-free, and double-free are the three most common C security vulnerabilities.