Pointers are the single concept that separates programmers who understand C from programmers who copy code until it works. Once you see what is actually happening in memory, everything clicks. This is that explanation.

What a Variable Actually Is

Every variable in your program occupies a location in memory. That location has an address — a number that identifies exactly where in RAM the data lives.

#include <stdio.h>

int main() {
    int x = 42;
    printf("Value of x:   %dn", x);
    printf("Address of x: %pn", (void*)&x);
    return 0;
}

Output (addresses vary per run):

Value of x:   42
Address of x: 0x7ffee4b2c8ac
Memory:
  Address        Value
  0x7ffee4b2c8ac  [ 42 ]   ← x lives here

The & operator gives you the address of a variable. That address is just a number.

What a Pointer Is

A pointer is a variable that stores an address.

#include <stdio.h>

int main() {
    int x = 42;
    int *p = &x;   /* p holds the address of x */

    printf("x     = %dn", x);
    printf("&x    = %pn", (void*)&x);
    printf("p     = %pn", (void*)p);    /* same as &x */
    printf("*p    = %dn", *p);          /* value at the address p holds */

    return 0;
}
Memory:
  Address        Value
  0x7ffee4b2c8ac  [ 42 ]       ← x
  0x7ffee4b2c8b0  [ 0x7ffee4b2c8ac ] ← p (stores the address of x)

*p is called dereferencing — it follows the address stored in p and reads the value at that location. *p and x both give you 42.

Modifying a Variable Through a Pointer

#include <stdio.h>

int main() {
    int x = 42;
    int *p = &x;

    *p = 100;   /* write 100 to the address stored in p */

    printf("x = %dn", x);   /* prints 100 — x was modified through p */
    return 0;
}

This is what makes pointers powerful. A function can receive a pointer and modify the original variable in the caller — not a copy of it.

Pointers as Function Parameters

C passes all arguments by value. Without pointers, a function cannot modify the caller’s variables:

#include <stdio.h>

void add_ten_wrong(int n) {
    n += 10;   /* modifies the local copy only */
}

void add_ten_right(int *n) {
    *n += 10;  /* modifies the value at the address n points to */
}

int main() {
    int x = 5;

    add_ten_wrong(x);
    printf("After wrong: %dn", x);   /* still 5 */

    add_ten_right(&x);
    printf("After right: %dn", x);   /* 15 */

    return 0;
}

Pointer Arithmetic

You can add integers to pointers. The increment is scaled by the size of the pointed-to type.

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;   /* p points to arr[0] */

    printf("*p       = %dn", *p);       /* 10 */
    printf("*(p + 1) = %dn", *(p+1));   /* 20 */
    printf("*(p + 2) = %dn", *(p+2));   /* 30 */

    /* incrementing the pointer */
    p++;
    printf("after p++: *p = %dn", *p);  /* 20 */

    return 0;
}
Memory (assuming 4-byte ints):
  Address   Value
  1000      [ 10 ]  ← arr[0], p initially points here
  1004      [ 20 ]  ← arr[1], p+1 points here
  1008      [ 30 ]  ← arr[2], p+2 points here

Adding 1 to an int* moves forward by 4 bytes (the size of int). Adding 1 to a char* moves forward by 1 byte. The compiler handles the multiplication automatically.

Double Pointers (Pointer to a Pointer)

#include <stdio.h>

int main() {
    int x = 42;
    int *p = &x;
    int **pp = &p;   /* pp holds the address of p */

    printf("x   = %dn", x);
    printf("*p  = %dn", *p);
    printf("**pp = %dn", **pp);   /* same as x */

    **pp = 99;
    printf("x after **pp = 99: %dn", x);   /* 99 */

    return 0;
}
Memory:
  Address   Value
  1000      [ 42 ]        ← x
  1008      [ 1000 ]      ← p (points to x)
  1016      [ 1008 ]      ← pp (points to p)

Double pointers are used when a function needs to modify a pointer in the caller — for example, a function that allocates memory and updates the caller’s pointer.

NULL — the Zero Address

int *p = NULL;   /* p holds address 0 — points to nothing */

if (p == NULL) {
    printf("p is not pointing to anythingn");
}

/* Never dereference NULL */
*p = 42;   /* segfault — address 0 is not accessible */

NULL is a convention for “this pointer is not valid.” Always initialize pointers to NULL when you do not have a valid address yet, and always check before dereferencing.

Common Pointer Mistakes

/* 1. Uninitialized pointer — points to garbage address */
int *p;
*p = 42;   /* undefined behaviour */

/* 2. Forgetting & when calling a function */
int x = 5;
scanf("%d", x);    /* wrong — passing the value */
scanf("%d", &x);  /* right — passing the address */

/* 3. Pointer type mismatch */
float f = 3.14;
int *p = &f;   /* wrong type — reading through p gives garbage */

TL;DR

  • A pointer stores a memory address, not a value
  • &x gives the address of x — use this to create a pointer to x
  • *p dereferences a pointer — reads or writes the value at the stored address
  • Pointer arithmetic is scaled by the size of the pointed-to type
  • Always initialize pointers; check for NULL before dereferencing
  • Double pointers (int**) let functions modify the caller’s pointer
  • Once you are comfortable with these basics, explore function pointers — pointers that store the address of a function instead of data