ccompiler.inOnline C Compiler & Docs
Engineering Blog2026-08-1410 min read

Master C pointers with visual diagrams. Learn memory addresses, dereferencing, pointer arithmetic, pass-by-reference, and double pointers.

Pointers are widely regarded as the most intimidating hurdle for developers learning the C programming language. Yet, pointers are also the very feature that gives C its unmatched speed, hardware-level control, and efficiency.

Without pointers, you cannot dynamically allocate memory on the heap, implement efficient data structures like linked lists and binary trees, or modify function arguments by reference.

In this comprehensive visual guide, you will master C pointers from the ground up. We will break down memory architectures, the & and * operators, pointer arithmetic, array decay, double pointers, and common memory traps with runnable C code examples.


1. What is a Pointer? The Fundamental Mental Model

To understand pointers, you must first understand how your computer's RAM stores variables.

Think of system memory (RAM) as a massive street of contiguous numbered mailboxes. Each mailbox is 1 byte (8 bits) in size and has a unique numerical memory address (usually displayed in hexadecimal format, such as 0x7FFE4B20).

When you declare a regular variable:

c (ISO Standard)
int score = 42;

The compiler reserves 4 contiguous bytes in memory on the Stack and associates the label score with that memory location.

text (ISO Standard)
+-------------------+-------------------+-------------------+
| Variable Label    | score             | ptr               |
| Variable Type     | int (4 bytes)     | int* (8 bytes)    |
| Value Stored      | 42                | 0x1000            |
| Memory Address    | 0x1000            | 0x2000            |
+-------------------+-------------------+-------------------+

A Pointer is simply a variable whose stored value is the memory address of another variable.

  • Regular variable: stores data (e.g., 42, 3.14, 'A').
  • Pointer variable: stores a memory address (e.g., 0x1000).

2. The Two Core Operators: & (Address-Of) and * (Dereference)

Working with pointers in C revolves around two unary operators:

  1. Address-Of Operator (&): Extracts the memory address where a variable lives.
  2. Dereference / Indirection Operator (*): Accesses or modifies the value stored at the memory address held by the pointer.

Runnable Code Example: Address-Of & Dereferencing

c (ISO Standard)
#include <stdio.h>

int main(void) {
    int age = 25;
    int *ptr = &age; // 'ptr' stores the memory address of 'age'

    printf("1. Value of age: %d\n", age);
    printf("2. Memory address of age (&age): %p\n", (void *)&age);
    printf("3. Value stored in ptr (address): %p\n", (void *)ptr);
    printf("4. Memory address of ptr itself (&ptr): %p\n", (void *)&ptr);
    printf("5. Dereferenced value (*ptr): %d\n", *ptr);

    // Modifying the original variable via the pointer
    *ptr = 30;
    printf("6. New value of age after '*ptr = 30': %d\n", age);

    return 0;
}

Expected Terminal Output:

text (ISO Standard)
1. Value of age: 25
2. Memory address of age (&age): 0x7ffd9b8a4f14
3. Value stored in ptr (address): 0x7ffd9b8a4f14
4. Memory address of ptr itself (&ptr): 0x7ffd9b8a4f18
5. Dereferenced value (*ptr): 25
6. New value of age after '*ptr = 30': 30

Notice that changing *ptr = 30 directly altered the value of age. Because ptr holds the memory address of age, writing through *ptr directly overwrites the bytes residing at that address.


3. Pointer Syntax: How to Read Pointer Declarations

Pointer syntax often confuses newcomers because the asterisk (*) serves two distinct purposes depending on where it appears:

c (ISO Standard)
int *ptr;  // Purpose 1: Declaration (defines 'ptr' as a pointer to an int)
*ptr = 100; // Purpose 2: Dereference (stores 100 at the target address)

The "Right-to-Left" Rule for Complex Declarations

To read any C pointer declaration, read from right to left:

DeclarationHow to ReadExplanation
int *p;p is a pointer to an intStandard integer pointer
const int *p;p is a pointer to a const intYou cannot modify *p, but p can point elsewhere
int * const p;p is a const pointer to an intp cannot change address, but *p can be modified
const int * const p;p is a const pointer to a const intNeither the address nor the value can change
int **p;p is a pointer to a pointer to an intDouble pointer

4. Pointer Arithmetic: How Offsets Work

When you add or subtract an integer from a pointer, the compiler does not simply increment the raw memory address by 1 byte. Instead, it scales the addition by sizeof(target_type).

text (ISO Standard)
Address formula:
New_Address = Current_Address + (n * sizeof(DataType))

Pointer Arithmetic Code Example

c (ISO Standard)
#include <stdio.h>

int main(void) {
    int numbers[4] = {10, 20, 30, 40};
    int *p = numbers; // Points to numbers[0]

    printf("sizeof(int): %zu bytes\n\n", sizeof(int));

    for (int i = 0; i < 4; i++) {
        printf("Pointer address: %p | Dereferenced *(p + %d): %d\n", 
               (void *)(p + i), i, *(p + i));
    }

    return 0;
}

Output Breakdown:

text (ISO Standard)
sizeof(int): 4 bytes

Pointer address: 0x7ffe1000 | Dereferenced *(p + 0): 10
Pointer address: 0x7ffe1004 | Dereferenced *(p + 1): 20
Pointer address: 0x7ffe1008 | Dereferenced *(p + 2): 30
Pointer address: 0x7ffe100c | Dereferenced *(p + 3): 40

Notice that p + 1 advances the memory address by 4 bytes because sizeof(int) == 4. If p were a double *, p + 1 would advance by 8 bytes.


5. Pointers and Arrays: Understanding Array Decay

In C, an array name is not a distinct pointer variable, but in almost all expressions, the array name decays into a pointer to its first element (&arr[0]).

Under the hood, bracket notation arr[i] is purely syntactic sugar for pointer arithmetic:

text (ISO Standard)
arr[i]  <===>  *(arr + i)
c (ISO Standard)
#include <stdio.h>

int main(void) {
    int arr[] = {100, 200, 300};

    // Both lines below access the exact same memory location:
    printf("arr[1]     = %d\n", arr[1]);
    printf("*(arr + 1) = %d\n", *(arr + 1));

    // Fun fact: By commutativity of addition, 1[arr] also works!
    printf("1[arr]     = %d\n", 1[arr]); 

    return 0;
}

6. Pass-by-Value vs. Pass-by-Reference in Functions

By default, C passes all function arguments by value (it creates a local copy). If you want a function to modify a variable declared in the caller's scope, you must pass a pointer (simulate pass-by-reference).

Classic Example: The Swap Function

Incorrect (Pass-by-Value):

c (ISO Standard)
void swapWrong(int a, int b) {
    int temp = a;
    a = b;
    b = temp; // Only swaps local copies inside swapWrong stack frame!
}

Correct (Pass-by-Reference with Pointers):

c (ISO Standard)
#include <stdio.h>

void swapCorrect(int *a, int *b) {
    int temp = *a; // Read value at address 'a'
    *a = *b;       // Write value from address 'b' into address 'a'
    *b = temp;     // Write stored temp value into address 'b'
}

int main(void) {
    int x = 10, y = 20;

    printf("Before swap: x = %d, y = %d\n", x, y);
    swapCorrect(&x, &y); // Pass memory addresses
    printf("After swap:  x = %d, y = %d\n", x, y);

    return 0;
}

Visualizing Stack Frames during swapCorrect(&x, &y):

text (ISO Standard)
[main Stack Frame]
  x = 10  (Address: 0x100)
  y = 20  (Address: 0x104)

[swapCorrect Stack Frame]
  a = 0x100 (Points to x in main)
  b = 0x104 (Points to y in main)
  temp = 10

7. Double Pointers (int **): Pointer to a Pointer

A Double Pointer is a pointer that stores the address of another pointer variable.

text (ISO Standard)
+----------+      +----------+      +----------+
|  ptr2    | ---> |   ptr1   | ---> |  value   |
| (0x3000) |      | (0x2000) |      |   (42)   |
| Address: |      | Address: |      | Address: |
|  0x3000  |      |  0x2000  |      |  0x1000  |
+----------+      +----------+      +----------+

Practical Use Case: Modifying a Pointer inside a Function

When allocating heap memory inside a helper function, passing a single pointer modifies only a local copy. You must pass a double pointer to reassign the caller's pointer address.

c (ISO Standard)
#include <stdio.h>
#include <stdlib.h>

void allocateBuffer(int **ptr, int size) {
    // *ptr modifies the caller's pointer variable directly
    *ptr = (int *)malloc(size * sizeof(int));
}

int main(void) {
    int *data = NULL;

    allocateBuffer(&data, 5); // Pass address of pointer variable

    if (data != NULL) {
        data[0] = 99;
        printf("data[0] = %d\n", data[0]);
        free(data);
        data = NULL;
    }

    return 0;
}

8. Void Pointers (void *) & Generic Memory Handling

A void * (or raw generic pointer) represents a memory address without a specific associated data type. Standard library functions like malloc(), memcpy(), and qsort() utilize void * to manipulate arbitrary data structures.

Rules for void *:

  1. You cannot direct-dereference a void * without typecasting it first (*void_ptr is invalid).
  2. You cannot perform pointer arithmetic on a standard ISO void * without casting.
c (ISO Standard)
#include <stdio.h>

void printGeneric(void *data, char type) {
    switch (type) {
        case 'i':
            printf("Integer: %d\n", *(int *)data);
            break;
        case 'f':
            printf("Float: %.2f\n", *(float *)data);
            break;
        case 'c':
            printf("Char: %c\n", *(char *)data);
            break;
    }
}

int main(void) {
    int num = 42;
    float pi = 3.14159f;
    char letter = 'Z';

    printGeneric(&num, 'i');
    printGeneric(&pi, 'f');
    printGeneric(&letter, 'c');

    return 0;
}

9. 5 Critical Pointer Pitfalls and How to Avoid Them

PitfallProblem DescriptionDefensive Fix
1. Uninitialized (Wild) PointerPointer contains random garbage address. Dereferencing causes random crashes.Always initialize to NULL (int *p = NULL;).
2. Dangling PointerPointer still references heap memory after free(p) was called.Immediately reset pointer: free(p); p = NULL;.
3. Memory LeakOverwriting a pointer without calling free() loses the heap block forever.Always match every malloc() with a corresponding free().
4. Array Out-of-Bounds*(arr + 10) on an array of size 5 accesses unauthorized stack memory.Check index limits before applying pointer offsets.
5. Returning Local Stack PointerReturning &local_var from a function returns invalid, deallocated stack memory.Allocate return data dynamically with malloc() or pass caller buffer.

Frequently Asked Questions (FAQ)

1. What is the difference between NULL, '\0', and 0 in C?

  • 0: The integer literal zero.
  • '\0': The null character constant (1 byte with value 0), used to terminate C strings.
  • NULL: A standard macro representing a null pointer constant (typically ((void *)0) or 0). In modern C23, use the native nullptr keyword.

2. How much memory does a pointer variable occupy?

The size of a pointer depends strictly on your computer's CPU architecture, regardless of what type it points to:

  • On a 32-bit system: all pointers (char*, int*, double*) are 4 bytes (32 bits).
  • On a 64-bit system: all pointers are 8 bytes (64 bits).

3. Why does sizeof(arr) give total bytes, but inside a function it gives 8 bytes?

When an array is passed into a function as an argument, it decays into a pointer (int arr[] becomes int *arr). Therefore, sizeof(arr) inside the function returns the pointer size (8 bytes on 64-bit systems) rather than the array's full element count.

4. What is a function pointer in C?

A function pointer stores the address of compiled machine instructions in the Text Segment. It allows passing functions as arguments (callbacks), such as the comparison function in qsort():

c (ISO Standard)
int compare(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

Conclusion

Pointers are not magic—they are simply variables that hold numerical memory addresses. Once you visualize the memory mailbox model, master the & and * operators, and practice defensive habits like NULL checks and post-free resets, pointers become your most powerful tool in systems programming.


Related Articles & References