<< Home

Chapter 9: Managing Pointers and Memory

Pointers are central to the power and flexibility of C. They allow direct access to memory, efficient manipulation of large data structures, and communication between functions through shared references. But they also introduce many of the language’s most difficult problems, including segmentation faults, memory leaks, and subtle logic errors that can be hard to trace. This chapter explores how pointers work, how to use them safely, and how to manage memory dynamically using the standard library.

Understanding pointers and addresses

A pointer is a variable that holds the memory address of another variable or object. Every object in memory occupies one or more bytes, and a pointer gives you a way to locate it. The & operator retrieves the address of a variable, and the * operator dereferences a pointer to access or modify the value stored at that address.

int x = 42;
int *p = &x;         // p holds the address of x
printf("%d\n", *p);  // prints 42

Pointers have types, and the compiler uses those types to interpret the bytes being accessed. A pointer to int is not interchangeable with a pointer to double because the data sizes differ. Correct typing ensures proper arithmetic, alignment, and interpretation of memory contents.

💡 A null pointer (NULL or 0) points to nothing. Always initialize pointers to NULL when declaring them if you do not yet have a valid address.

Using pointer arithmetic

Pointers can participate in arithmetic, but operations are scaled by the size of the object type they point to. Incrementing an int * moves the pointer forward by sizeof(int) bytes, not by one raw byte. This allows natural iteration through arrays and other contiguous memory blocks.

int data[] = {10, 20, 30};
int *p = data;       // same as &data[0]
p++;                 // now points to data[1]
printf("%d\n", *p);  // prints 20

Pointer subtraction is also defined when both pointers refer to elements of the same array. The result is the number of elements between them. Arithmetic on unrelated pointers is undefined behavior and must be avoided.

⚠️ Do not assume that adding arbitrary integers to a pointer is safe. The pointer must always refer to memory within (or just past the end of) the same object.

Arrays and pointers compared

Arrays and pointers are closely related in C. When an array is used in an expression, it normally decays to a pointer to its first element. This is why function parameters that accept arrays are declared as pointer types; they receive the address of the first element, not a copy of the whole array.

void print_all(const int *a, size_t n) {
  for (size_t i = 0; i < n; i++) {
    printf("%d ", a[i]);
  }
  printf("\n");
}

int nums[] = {1, 2, 3, 4};
print_all(nums, 4);  // passes a pointer to nums[0]

However, arrays are not pointers. Their size and storage duration are determined at declaration, while pointers are independent variables that can change what they point to. The distinction becomes important when using sizeof or when allocating memory dynamically.

💡 Remember that sizeof array gives the total byte size of the array, while sizeof pointer gives only the size of the pointer variable itself.

Dynamic memory

Static arrays have fixed size, but sometimes you need memory that grows or shrinks at runtime. The C standard library provides four key functions for this purpose, declared in <stdlib.h>:

FunctionPurpose
malloc(n)Allocates n bytes, returns a pointer to uninitialized memory.
calloc(c, n)Allocates space for c objects of n bytes each, initializing all bits to zero.
realloc(p, n)Changes the size of a previously allocated block, preserving existing data up to the new size.
free(p)Releases a block of memory previously allocated.
#include <stdlib.h>

int *arr = malloc(5 * sizeof(int));
if (!arr) {
  perror("malloc failed");
  exit(EXIT_FAILURE);
}
for (int i = 0; i < 5; i++) arr[i] = i * 10;
arr = realloc(arr, 10 * sizeof(int));
free(arr);

Dynamic allocation moves storage to the heap rather than the stack. It gives you flexibility at the cost of manual management. Forgetting to call free() results in memory leaks, while freeing memory twice leads to undefined behavior.

⚠️ Always pair every successful allocation with exactly one corresponding free(). Consider setting the pointer to NULL after freeing it to avoid accidental reuse.

Common pointer errors and debugging techniques

Most serious C bugs arise from pointer misuse. The most common include dereferencing a null or uninitialized pointer, using a pointer after freeing its memory, and writing beyond allocated bounds. Such mistakes often cause segmentation faults or data corruption that appears far from the source of the error.

To diagnose pointer problems, use debugging tools such as gdb, memory checkers like valgrind, and compiler sanitizers (-fsanitize=address with GCC or Clang). These tools detect invalid accesses, double frees, and leaks by instrumenting your program’s memory operations.

// Example of a bad pointer bug
int *p;
*p = 5;  // undefined behavior: p is uninitialized
💡 Compile with -Wall -Wextra -Werror to catch many potential pointer issues at compile time before they become runtime bugs.

Segmentation faults and alignment

Memory in a C program is divided into regions: the stack, the heap, and fixed areas for global data and code. The stack stores automatic variables and function call frames. The heap holds dynamically allocated memory. Stack memory is managed automatically, while heap memory must be explicitly allocated and freed.

A segmentation fault occurs when a program tries to access memory outside its allowed region, such as dereferencing an invalid pointer or writing to read-only space. These errors often trace back to missing checks or improper pointer arithmetic.

Memory alignment ensures that data types begin at addresses suited to their size (for example, a 4-byte integer aligned on a 4-byte boundary). Misaligned access can slow down performance or even crash on certain architectures. The compiler handles alignment automatically for normal variables, but when using raw pointers, you must be cautious to maintain correct alignment.

⚠️ Understanding the memory layout (stack, heap, globals, constants) helps you interpret crash reports and stack traces. Many difficult bugs become clear once you visualize where each pointer lives and what lifetime it has.