C Pointers Explained
Understand C pointers from first principles: addresses, dereferencing, pointer arithmetic, arrays, common bugs like null dereferences and dangling pointers, and how to use ASan and Valgrind.
Before you start
- ▸Basic C syntax knowledge (variables, loops, functions)
- ▸A C compiler installed: gcc or clang
- ▸Valgrind installed for memory analysis (optional but recommended)
- ▸Familiarity with compiling and running C programs from the command line
Pointers are the feature that separates C from higher-level languages — and the feature most responsible for subtle, hard-to-reproduce bugs. They are not magic: a pointer is simply a variable whose value is a memory address. Once that clicks, everything else — arrays, function arguments, dynamic allocation — becomes a matter of arithmetic and discipline.
What a Pointer Actually Is
Every variable lives at a specific address in memory. On a 64-bit system that address is an 8-byte unsigned integer. A pointer stores that integer so you can read or write the variable it refers to indirectly.
cat pointer_basic.c
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; /* p holds the address of x */
printf("value of x : %d\n", x);
printf("address of x: %p\n", (void *)p);
printf("via pointer : %d\n", *p); /* dereference */
return 0;
}
Two operators do all the work: & (address-of) gives you a pointer to a variable; * (dereference) follows a pointer to the value it points at. The type of the pointer (int *, char *, etc.) tells the compiler how many bytes to read and how to interpret them.
Pointers and Arrays
In C, an array name decays to a pointer to its first element in almost every expression. That means array indexing and pointer arithmetic are two spellings of the same operation.
#include <stdio.h>
int main(void) {
int arr[4] = {10, 20, 30, 40};
int *p = arr; /* same as &arr[0] */
/* These four expressions are identical */
printf("%d %d %d %d\n", arr[2], *(arr+2), p[2], *(p+2));
return 0;
}
The compiler converts arr[i] to *(arr + i) automatically. Adding 1 to an int * advances the address by sizeof(int) bytes (typically 4), not by 1 byte. This is pointer arithmetic, and it only makes sense within a single allocated region.
Pointer Arithmetic in Detail
Legal arithmetic on a pointer p of type T *:
p + n— advance byn * sizeof(T)bytesp - n— retreat by the samep2 - p1— difference in elements (result typeptrdiff_t), valid only when both point into the same array- Comparisons (
<,>,==) — valid within the same array, including one-past-the-end
#include <stdio.h>
#include <stddef.h>
int main(void) {
int arr[5] = {1, 2, 3, 4, 5};
int *start = arr;
int *end = arr + 5; /* one-past-the-end: valid to hold, not to dereference */
ptrdiff_t len = end - start;
printf("length: %td\n", len); /* prints 5 */
return 0;
}
Dereferencing the one-past-the-end pointer (*end above) is undefined behaviour. Reading it without dereferencing — as a sentinel for loops — is explicitly allowed by the C standard.
Pointers as Function Parameters
C passes all arguments by value. To let a function modify a caller's variable, pass a pointer to it. This is also how you avoid copying large structs.
#include <stdio.h>
void double_it(int *n) {
*n *= 2; /* modifies the caller's variable */
}
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int x = 7, y = 3;
double_it(&x);
swap(&x, &y);
printf("x=%d y=%d\n", x, y); /* x=6 y=14 */
return 0;
}
const and Pointer Types
The position of const changes meaning significantly. Get this wrong and you either lose the compiler's protection or create a type mismatch.
| Declaration | What is read-only |
|---|---|
const int *p | The pointed-at value (pointer itself is mutable) |
int * const p | The pointer (value it points at is mutable) |
const int * const p | Both |
Pass const char * to functions that only read a string. The compiler will warn if such a function tries to write through the pointer.
Common Pointer Bugs
Uninitialized Pointers
A local pointer variable starts with a garbage address. Dereferencing it is undefined behaviour and can corrupt memory silently before crashing.
int *p; /* garbage address */
*p = 5; /* undefined behaviour — do NOT do this */
Always initialize: to NULL, to &something, or to the return value of malloc before dereferencing.
NULL Dereference
Functions like malloc, fopen, and many POSIX calls return NULL on failure. Dereferencing without checking is the single most common C crash.
#include <stdlib.h>
int *p = malloc(sizeof(int) * 100);
if (p == NULL) {
/* handle allocation failure */
return 1;
}
/* safe to use p here */
Dangling Pointers
A pointer becomes dangling when the memory it points to is freed or goes out of scope. Reading or writing through it is undefined behaviour.
#include <stdlib.h>
int *p = malloc(sizeof(int));
*p = 99;
free(p);
/* p is now dangling */
p = NULL; /* good practice: null it immediately after free */
Buffer Overruns
Walking a pointer past the end of an allocated region overwrites adjacent memory — a classic security vulnerability. Always track your bounds explicitly.
char buf[8];
char *p = buf;
for (int i = 0; i < 8; i++, p++) {
*p = 'A'; /* fine */
}
*p = 'Z'; /* one past the end — undefined behaviour */
Pointer Type Aliasing
Casting a pointer to an incompatible type and then dereferencing violates strict aliasing rules. The exception is char *, which may alias any type. Use memcpy when you genuinely need to reinterpret bytes.
Reasoning About Pointers: A Mental Model
Think of memory as a long strip of numbered boxes. A pointer is a slip of paper with a box number written on it. Dereferencing means walking to that box and reading or writing its contents. Pointer arithmetic means adding to the number on the slip before walking there. With that model:
- An uninitialized pointer is a slip with a random number — you do not know which box you will reach.
- A NULL pointer is a slip marked zero — by convention, no object lives there; checking for NULL before dereferencing is like checking whether the slip is blank before using it.
- A freed pointer is a slip whose box has been reassigned — the box still exists at that address, but writing to it corrupts whoever owns it now.
- A one-past-the-end pointer is a valid slip you may hold but must not use to access any box.
Verifying Your Code with Tooling
The compiler and runtime tools catch most pointer bugs before they reach production.
# Compile with address sanitizer (gcc and clang both support this)
gcc -fsanitize=address,undefined -g -o pointer_test pointer_test.c
./pointer_test
# Valgrind: detailed memory error and leak detection
valgrind --leak-check=full ./pointer_test
AddressSanitizer (ASan) adds roughly 2× overhead and is suitable for test builds. It catches out-of-bounds reads, use-after-free, and stack buffer overflows at the exact source line. -fsanitize=undefined (UBSan) catches null dereferences and signed integer overflow. Run both during development, not just in CI.
Troubleshooting
- Segmentation fault on first dereference: pointer was never initialized or the function that was supposed to return a pointer returned NULL. Add a NULL check and run under ASan to get the exact line.
- Corruption that appears far from the bug: classic buffer overrun. Compile with ASan; it reports the overrun at the write, not at the eventual crash.
- Valgrind reports "Invalid read" only in optimised builds: the compiler is reusing stack space aggressively. Build with
-O0 -gfor diagnostic runs. - Double-free crash: two code paths both call
freeon the same pointer. Null the pointer immediately after freeing;free(NULL)is a safe no-op. - Warnings about discarding
const: you are passing aconst T *where aT *is expected, or vice versa. Fix the function signature, not the warning.
Frequently asked questions
- What is the difference between a pointer and an array in C?
- An array is a fixed-size block of storage; a pointer is a variable holding an address. An array name decays to a pointer in most expressions, but sizeof(array) returns the full byte size while sizeof(pointer) returns the pointer width (typically 8 bytes on 64-bit systems).
- When should I use NULL versus 0 to initialise a pointer?
- Use NULL. Both are technically equivalent in C, but NULL communicates intent — this variable holds no valid address — and prevents accidental arithmetic on a zero-valued integer.
- Is it safe to do arithmetic on a void pointer?
- The C standard does not permit arithmetic on void * because the element size is undefined. GCC permits it as an extension, treating void * like char *. For portable code, cast to char * or an appropriate type first.
- Why does freeing a pointer not set it to NULL automatically?
- free() receives a copy of the pointer value, so it cannot modify the caller's variable. It is the programmer's responsibility to null the pointer after freeing. Some teams wrap free in a macro for this reason.
- What is undefined behaviour and why does it matter so much with pointers?
- Undefined behaviour means the C standard places no constraints on what the program does — the compiler is free to assume it never happens and optimise accordingly. This can make pointer bugs appear to work in debug builds but produce silent corruption or security vulnerabilities in optimised production builds.
Related guides
Bash Arrays and Associative Arrays
Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.
Bash Functions and Variable Scoping
Master Bash function scoping with local variables, source-based libraries, correct use of return codes, and array passing techniques including namerefs.
Bash Loops: for, while and until
Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.
Bash Scripting for Beginners
Learn Bash scripting from scratch: shebang lines, variables, conditionals, loops, and arguments, plus a real backup script to tie it all together.