Programming

C Pointers and Memory Layout

·2 min read

C is unforgiving about memory because C doesn't manage memory for you. Every other complaint about C — segfaults, undefined behaviour, alignment errors — traces back to a small set of concepts. Internalize them and the language gets dramatically less mysterious.

What a pointer is

A pointer is a variable that holds an address. The type of the pointer tells the compiler how to interpret the bytes at that address and how to do pointer arithmetic.

int x = 42;
int *p = &x;        // p holds the address of x
*p = 100;            // write 100 to the address — x is now 100
printf("%d\n", *p);  // 100

Stack vs heap

Local variables live on the stack — a region of memory that grows and shrinks with function calls. They're automatically freed when the function returns. Memory from malloc lives on the heap and persists until you free it.

int *bad(void) {
    int x = 42;
    return &x;       // UNDEFINED: x dies when bad() returns
}

int *good(void) {
    int *x = malloc(sizeof(int));
    *x = 42;
    return x;        // OK — caller must free
}

Arrays and pointers are not the same

A widespread misconception. An array name decays to a pointer to its first element in most contexts, but it isn't a pointer. Two big differences: sizeof on an array gives the array size in bytes; on a pointer it gives the pointer size (8 on 64-bit). And the address of an array isn't the same type as a pointer to its element type.

int arr[10];
int *p = arr;        // OK — decay

sizeof(arr);          // 40 (10 * sizeof(int))
sizeof(p);            // 8 on 64-bit

&arr;                 // type: int (*)[10]
&arr[0];              // type: int *

Pointer arithmetic respects the type

int *p = arr;
p + 1;       // address + sizeof(int), not address + 1
p[3];        // syntactic sugar for *(p + 3)

const placement

Read const right-to-left around the asterisk:

const int *p;    // pointer to const int — can't modify *p
int *const p;    // const pointer to int — can't change p
const int *const p;  // both

The strict aliasing rule

The compiler assumes pointers of different types don't point to the same object (with specific exceptions: char* can alias anything, signed/unsigned variants of the same type, etc.). Violating this is undefined behaviour, and modern optimizers will use the assumption aggressively.

// UB — reads the bytes of a float through an int*
float f = 1.0f;
int *ip = (int *)&f;
int bits = *ip;

// Defined — use memcpy
int bits;
memcpy(&bits, &f, sizeof(bits));

Tools that catch what the compiler won't

  • AddressSanitizer (-fsanitize=address): catches use-after-free, heap-buffer-overflow, stack-buffer-overflow, double-free.
  • UndefinedBehaviorSanitizer (-fsanitize=undefined): catches signed integer overflow, misaligned loads, division by zero.
  • Valgrind: slower but excellent for tracking memory leaks and uninitialized reads.
  • Clang's -Wall -Wextra -Wpedantic: turn it all on, treat warnings as errors.

Related