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.
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.
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.
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>:
| Function | Purpose |
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.
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
-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.