Pointers and Memory

A pointer is a variable that holds a memory address. Pointer arithmetic, combined with C’s manual memory model, gives you direct control over how data is stored and accessed — and direct responsibility for getting it right.

Why It Matters

Every segfault, buffer overflow, and use-after-free bug traces back to pointer misuse. Understanding pointers means understanding how data lives in memory — essential for systems programming, embedded development, and debugging anything written in C.

Stack vs Heap

High addresses
┌──────────────────┐
│      Stack       │  ← local variables, grows DOWN
│        ↓         │
│                  │
│        ↑         │
│      Heap        │  ← malloc'd memory, grows UP
├──────────────────┤
│    BSS (zeros)   │  ← uninitialized globals
├──────────────────┤
│   Data (init'd)  │  ← initialized globals
├──────────────────┤
│      Text        │  ← compiled code (read-only)
└──────────────────┘
Low addresses

Stack allocation is automatic and fast (just move the stack pointer). Heap allocation (malloc) is flexible but slower and requires manual free.

Pointer Arithmetic

int arr[] = {10, 20, 30, 40};
int *p = arr;       // points to arr[0]
p++;                // moves sizeof(int) bytes forward → arr[1]
printf("%d\n", *p); // 20
 
// Array indexing IS pointer arithmetic
arr[2] == *(arr + 2)  // both give 30
 
// Pointer difference gives element count, not byte count
int *end = arr + 4;
printf("%td elements\n", end - arr);  // 4

Pointer Variants

void *vp;              // generic pointer — must cast before dereference
int **pp;              // pointer to pointer — used for dynamic 2D arrays
const int *p;          // can't modify the pointed-to value through p
int *const p = &x;     // can't change where p points

Double Pointers

// Common use: function that allocates memory for the caller
void alloc_buffer(char **buf, size_t size) {
    *buf = malloc(size);
}
 
char *data;
alloc_buffer(&data, 1024);  // data now points to heap memory
free(data);

Common Bugs

Dangling Pointer

int *p = malloc(sizeof(int));
*p = 42;
free(p);
// p still holds the old address — DANGLING
*p = 99;  // UNDEFINED BEHAVIOR — may crash, may silently corrupt
p = NULL; // fix: always null after free

Double Free

free(p);
free(p);  // heap corruption — exploitable security vulnerability

Buffer Overflow

char buf[8];
strcpy(buf, "this string is way too long");  // overwrites past buf!
// Fix: strncpy(buf, src, sizeof(buf) - 1); buf[sizeof(buf)-1] = '\0';

Use of Uninitialized Pointer

int *p;        // garbage address
*p = 42;       // writing to random memory — UB

Debugging with Valgrind

gcc -g -O0 -o program program.c
valgrind --leak-check=full ./program

Typical Valgrind output for a leak:

==12345== 40 bytes in 1 blocks are definitely lost
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/...)
==12345==    by 0x401145: main (program.c:8)
Valgrind ErrorMeaning
Invalid read/writeAccessing freed or out-of-bounds memory
Definitely lostMemory leaked — no pointer to it remains
Conditional jump on uninitializedUsing a variable before setting it

Compile with -fsanitize=address (ASan) as an alternative — faster than Valgrind, catches most of the same bugs at runtime.