$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

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 by n * sizeof(T) bytes
  • p - n — retreat by the same
  • p2 - p1 — difference in elements (result type ptrdiff_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.

DeclarationWhat is read-only
const int *pThe pointed-at value (pointer itself is mutable)
int * const pThe pointer (value it points at is mutable)
const int * const pBoth

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 -g for diagnostic runs.
  • Double-free crash: two code paths both call free on the same pointer. Null the pointer immediately after freeing; free(NULL) is a safe no-op.
  • Warnings about discarding const: you are passing a const T * where a T * is expected, or vice versa. Fix the function signature, not the warning.
tested on:Ubuntu 24.04Fedora 40Arch rollingDebian 12

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