C Pointers to Pointers double pointer chain with memory layout and 2D dynamic array
|

C Pointers to Pointers: Double & Triple Pointers Explained (2026)

C pointers to pointers (double pointers) seem intimidating at first glance. But they follow the same logic as regular pointers: they store an address. The only difference is that the address they store belongs to another pointer, not a regular variable. Once you grasp this one idea, double pointers become a natural extension of what you already know.

In this lesson from our C Roadmap, you will learn why double pointers exist, when to use them, and how they power dynamic 2D arrays, command-line arguments, and linked list operations. Make sure you understand C Pointers before continuing.

What Are C Pointers to Pointers?

A pointer stores the address of a variable. A pointer to a pointer stores the address of a pointer. It is one more level of indirection:

int x = 42;        // Regular variable
int *p = &x;       // Pointer to x
int **pp = &p;     // Pointer to pointer p

// Accessing the value:
printf("%d\n", x);     // 42 (direct)
printf("%d\n", *p);    // 42 (one dereference)
printf("%d\n", **pp);  // 42 (two dereferences)

Each level of * follows one address. *pp gives you p (the address of x). **pp follows p to get x (the value 42).

Declaring and Using C Double Pointers

#include <stdio.h>

int main(void) {
    int value = 100;
    int *ptr = &value;
    int **dptr = &ptr;

    printf("value       = %d\n", value);         // 100
    printf("*ptr        = %d\n", *ptr);           // 100
    printf("**dptr      = %d\n", **dptr);         // 100

    printf("\nAddresses:\n");
    printf("&value = %p\n", (void *)&value);     // Address of value
    printf("ptr    = %p\n", (void *)ptr);          // Same as &value
    printf("&ptr   = %p\n", (void *)&ptr);        // Address of ptr
    printf("dptr   = %p\n", (void *)dptr);         // Same as &ptr

    // Modify value through double pointer
    **dptr = 999;
    printf("\nAfter **dptr = 999:\n");
    printf("value = %d\n", value);  // 999

    // Change what ptr points to through double pointer
    int other = 555;
    *dptr = &other;  // Now ptr points to other
    printf("\nAfter *dptr = &other:\n");
    printf("*ptr = %d\n", *ptr);    // 555

    return 0;
}

Notice the key insight: **dptr = 999 modifies the final value. *dptr = &other changes what the intermediate pointer points to. This two-level control is what makes double pointers useful.

C Double Pointer Memory Layout

Here is a visual diagram showing how the three variables relate in memory:

Memory Address   Variable    Value           Points To
─────────────────────────────────────────────────────
0x1000           value       42              (just data)
0x1008           ptr         0x1000          → value
0x1010           dptr        0x1008          → ptr → value

  dptr [0x1008] ──→ ptr [0x1000] ──→ value [42]
  
  **dptr = 42
  *dptr  = 0x1000 (the pointer ptr itself)
  dptr   = 0x1008 (address of ptr)

Modifying Pointers via C Double Pointers

The primary use case for double pointers is allowing a function to modify a pointer in the caller’s scope. Remember from our Pointers & Functions lesson: to modify a variable from a function, you pass a pointer to it. To modify a pointer from a function, you pass a pointer to the pointer:

#include <stdio.h>
#include <stdlib.h>

// This function modifies the caller's pointer
void allocate_array(int **arr, int size) {
    *arr = malloc(size * sizeof(int));
    if (*arr == NULL) return;
    for (int i = 0; i < size; i++) {
        (*arr)[i] = i + 1;
    }
}

void safe_free(int **arr) {
    if (*arr != NULL) {
        free(*arr);
        *arr = NULL;  // Prevents dangling pointer
    }
}

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

    allocate_array(&data, 5);
    if (data != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("%d ", data[i]);
        }
        printf("\n");

        safe_free(&data);
        printf("data is %s\n", data == NULL ? "NULL" : "dangling");
    }

    return 0;
}

Without int **, the function could only modify the data the pointer points to, not the pointer itself.

Dynamic 2D Arrays with C Double Pointers

Double pointers are the standard way to create dynamically sized 2D arrays:

#include <stdio.h>
#include <stdlib.h>

int **create_matrix(int rows, int cols) {
    // Allocate array of row pointers
    int **matrix = malloc(rows * sizeof(int *));
    if (matrix == NULL) return NULL;

    // Allocate each row
    for (int i = 0; i < rows; i++) {
        matrix[i] = malloc(cols * sizeof(int));
        if (matrix[i] == NULL) {
            // Cleanup on failure
            for (int j = 0; j < i; j++) free(matrix[j]);
            free(matrix);
            return NULL;
        }
    }
    return matrix;
}

void free_matrix(int **matrix, int rows) {
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
}

int main(void) {
    int rows = 3, cols = 4;
    int **grid = create_matrix(rows, cols);

    if (grid == NULL) return 1;

    // Fill with values
    int count = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            grid[i][j] = count++;
        }
    }

    // Print
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", grid[i][j]);
        }
        printf("\n");
    }

    free_matrix(grid, rows);
    return 0;
}

This approach is different from a true 2D array (Multidimensional Arrays). A static 2D array has contiguous memory. A int ** dynamic 2D array has separate allocations for each row, connected through the pointer array. Each row can even have a different length (jagged array).

Command-Line Arguments: char **argv

The most common double pointer in C is argv in the main() function:

#include <stdio.h>

// Both of these are valid:
// int main(int argc, char *argv[])
// int main(int argc, char **argv)

int main(int argc, char **argv) {
    printf("Program: %s\n", argv[0]);
    printf("Number of arguments: %d\n", argc);

    for (int i = 1; i < argc; i++) {
        printf("  argv[%d] = \"%s\"\n", i, argv[i]);
    }

    // argv is a pointer to the first element of an array of char*
    // argv[0] is a char* pointing to the program name
    // argv[1] is a char* pointing to the first argument
    // argv[argc] is NULL (guaranteed by the standard)

    return 0;
}

If you run ./program hello world:

argv → [ "program" | "hello" | "world" | NULL ]
         argv[0]     argv[1]   argv[2]   argv[3]

Each argv[i] is a char * (a string). argv itself is a char ** (pointer to the first string pointer).

Array of Strings with C Double Pointers

Double pointers naturally represent collections of strings:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char **create_string_array(int count) {
    char **arr = malloc(count * sizeof(char *));
    return arr;
}

void free_string_array(char **arr, int count) {
    for (int i = 0; i < count; i++) {
        free(arr[i]);
    }
    free(arr);
}

int main(void) {
    const char *names[] = {"Alice", "Bob", "Charlie"};
    int n = 3;

    // Create a modifiable copy
    char **copies = create_string_array(n);
    for (int i = 0; i < n; i++) {
        copies[i] = malloc(strlen(names[i]) + 1);
        strcpy(copies[i], names[i]);
    }

    // Modify a string
    free(copies[1]);
    copies[1] = malloc(6);
    strcpy(copies[1], "David");

    for (int i = 0; i < n; i++) {
        printf("%s\n", copies[i]);
    }

    free_string_array(copies, n);
    return 0;
}

Understanding how C Strings and C String Functions work helps you manage these dynamic string arrays safely.

Linked Lists and C Double Pointers

Double pointers elegantly solve the “modify the head pointer” problem in linked lists:

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

// Using double pointer: can modify head
void push_front(Node **head, int data) {
    Node *new_node = malloc(sizeof(Node));
    new_node->data = data;
    new_node->next = *head;
    *head = new_node;    // Modifies the caller's head pointer
}

void print_list(const Node *head) {
    while (head != NULL) {
        printf("%d → ", head->data);
        head = head->next;
    }
    printf("NULL\n");
}

void free_list(Node **head) {
    while (*head != NULL) {
        Node *temp = *head;
        *head = (*head)->next;
        free(temp);
    }
}

int main(void) {
    Node *list = NULL;  // Empty list

    push_front(&list, 30);
    push_front(&list, 20);
    push_front(&list, 10);

    print_list(list);   // 10 → 20 → 30 → NULL

    free_list(&list);
    printf("After free: list is %s\n", list == NULL ? "NULL" : "not NULL");

    return 0;
}

Without double pointers, push_front could not update the head pointer in main. You would have to return the new head every time, which is error-prone.

Triple Pointers in C (and Beyond)

Triple pointers (int ***) exist but are rare. They appear when you need to modify a double pointer from a function, or for 3D dynamic arrays:

#include <stdio.h>
#include <stdlib.h>

// Allocate a dynamic 2D array and set the caller's pointer
void alloc_2d(int ***matrix, int rows, int cols) {
    *matrix = malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        (*matrix)[i] = calloc(cols, sizeof(int));
    }
}

int main(void) {
    int **grid = NULL;
    alloc_2d(&grid, 3, 4);   // Triple pointer: &(int **) = int ***

    grid[1][2] = 42;
    printf("grid[1][2] = %d\n", grid[1][2]);

    for (int i = 0; i < 3; i++) free(grid[i]);
    free(grid);

    return 0;
}

In practice, if you find yourself using triple pointers frequently, consider restructuring your code. Usually a struct can replace deep pointer chains.

Common C Double Pointer Mistakes

1. Forgetting to allocate the intermediate pointer:

int **pp;
**pp = 42;     // CRASH: pp is uninitialized, *pp is garbage

2. Confusing * levels:

int x = 10;
int *p = &x;
int **pp = &p;

*pp = &x;    // Sets p to point to x (correct)
**pp = 20;   // Sets x to 20 (correct)
pp = &x;     // TYPE ERROR: pp expects int**, not int*

3. Not freeing in the right order:

// WRONG: frees the outer array first, leaks inner arrays
free(matrix);

// RIGHT: free inner arrays first, then outer
for (int i = 0; i < rows; i++) free(matrix[i]);
free(matrix);

4. Mixing up int** dynamic arrays with real 2D arrays:

int arr[3][4];        // Contiguous memory, fixed size
int **dyn;            // Array of pointers, rows may not be contiguous
// Cannot pass int** where int[][4] is expected!

Practice Exercises

Exercise 1: Write a function void split_string(const char *str, char delimiter, char ***result, int *count) that splits a string and returns an array of substrings through a triple pointer.

Exercise 2: Implement a complete linked list with push_back, pop_front, insert_at, and delete_at functions, all using double pointer parameters.

Exercise 3: Write a program that takes command-line arguments, sorts them alphabetically using qsort() on argv, and prints the result.

Solution to Exercise 3
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int cmp(const void *a, const void *b) {
    return strcmp(*(const char **)a, *(const char **)b);
}

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("Usage: %s word1 word2 ...\n", argv[0]);
        return 1;
    }
    // Sort argv[1..argc-1]
    qsort(argv + 1, argc - 1, sizeof(char *), cmp);
    for (int i = 1; i < argc; i++) {
        printf("%s\n", argv[i]);
    }
    return 0;
}

Summary

C pointers to pointers add one more level of indirection. A double pointer (int **) stores the address of a pointer. Use them when a function needs to modify a pointer, for dynamic 2D arrays, for argv in main(), and for linked list head pointer manipulation. Always free in reverse order (inner allocations first). Triple pointers exist but should be rare. With this lesson, you have completed the pointers section of our C Roadmap. Next up: Dynamic Memory Allocation, where you will learn malloc, calloc, realloc, and free in depth.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *