Question

Pointers to Pointers in C: When and Why to Use Double Indirection

cpointers

Question

When should a pointer to a pointer (double indirection) be used in C? I understand that int **p is a pointer to another pointer, but why would a program need this level of indirection? Please explain with a practical example.

Short Answer

A pointer to a pointer is useful when a function must change the caller’s pointer itself, not only the value it points to. This page explains double indirection in C, including pointer allocation, output parameters, linked-list updates, and the important difference between int ** and a true two-dimensional array.

Concept

In C, a pointer stores the address of another object. A pointer to a pointer stores the address of a pointer.

int value = 42;
int *ptr = &value;
int **ptr_to_ptr = &ptr;

The relationships are:

  • value is an int containing 42.
  • ptr contains the address of value.
  • ptr_to_ptr contains the address of ptr.

Dereferencing follows one address at a time:

*ptr            // value, which is 42
*ptr_to_ptr     // ptr, an int *
**ptr_to_ptr    // value, which is 42

The key reason for using T ** is that C passes function arguments by value. If you pass a T * to a function, the function receives a copy of that pointer. It can modify the pointed-to object, but assigning a new address to its local pointer does not update the caller’s pointer.

Pass the address of the pointer—using T **—when the function needs to replace, allocate, clear, or otherwise update the caller’s pointer variable.

This matters in real programs because many operations naturally need to update an address: allocating memory, returning a newly created object, inserting at the beginning of a linked list, or receiving an array of strings from an API.

Mental Model

Think of a pointer as a note containing a house address.

  • A value is the item inside the house.
  • A pointer is a note that tells you which house contains the item.
  • A pointer to a pointer is the location of the note itself.

If a function only needs to change something in the house, give it the address note (T *).

If the function needs to replace the address written on the note—for example, point it at a newly built house—give it the location of the note (T **). That lets it rewrite the caller’s pointer.

Take Quiz

Syntax and Examples

The general declaration is:

Type **name;

For example:

int number = 10;
int *first = &number;
int **second = &first;

printf("%d\n", number);    // 10
printf("%d\n", *first);    // 10
printf("%d\n", **second);  // 10

second points to first, and first points to number.

Updating a caller's pointer

This function allocates an integer and stores its address in the caller’s pointer:

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

int create_number(int **out_number) {
    *out_number = malloc(sizeof **out_number);

    if (*out_number == NULL) {
        return 0;
    }

    **out_number = 99;
    return 1;
}

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

    if (create_number(&number)) {
        printf("%d\n", *number);  // 99
        free(number);
    }

    return 0;
}

number is an int *. The expression &number is its address, so its type is int **. Inside create_number:

  • *out_number means number in main.
  • **out_number means the allocated int value.

Using sizeof **out_number is a robust allocation style: it allocates space for the final pointed-to object, an int in this example.

Step by Step Execution

Trace this small example:

#include <stdio.h>

void reset_pointer(int **target) {
    *target = NULL;
}

int main(void) {
    int value = 7;
    int *pointer = &value;

    reset_pointer(&pointer);

    printf("%p\n", (void *)pointer);
    return 0;
}
  1. value stores 7.
  2. pointer stores the address of value.
  3. &pointer is the address of the pointer variable itself. Its type is int **.
  4. reset_pointer receives that address in target.
  5. *target accesses the original pointer variable in main.
  6. Assigning NULL to *target changes pointer in main.
  7. The printed pointer value is a null pointer, commonly displayed as (nil) or 0x0; the exact formatting is implementation-dependent.

By contrast, this does not reset the caller’s pointer:

void does_not_reset(int *target) {
    target = NULL;  // Changes only the local copy.
}

Here, target is a copied pointer value. Reassigning it has no effect outside the function.

Real World Use Cases

Common uses of pointers to pointers in C include:

  • Memory allocation helpers: A function calls malloc and returns the allocated address through an output parameter.
  • Updating a linked-list head: Inserting or deleting the first node changes the head pointer.
  • APIs with output parameters: A library function may create an object and write its address into a caller-provided pointer variable.
  • Arrays of strings: Command-line arguments use char **argv; each element is a char * pointing to a string.
  • Dynamically allocated table representations: An array of row pointers can be represented by int **rows, where each rows[i] points to a row.

For example, inserting a node at the start of a singly linked list must change the caller’s head pointer:

struct Node {
    int value;
    struct Node *next;
};

void prepend(struct Node **head, struct Node *node) {
    node->next = *head;
    *head = node;
}
Take Quiz

Real Codebase Usage

In production C code, T ** commonly appears in APIs that return or replace an owned resource.

Output parameters with error handling

A function can report success with a return value and provide a created object through an output parameter:

#include <stdlib.h>

int make_buffer(size_t size, unsigned char **out_buffer) {
    if (out_buffer == NULL) {
        return 0;
    }

    *out_buffer = NULL;  // Safe state on failure.

    if (size == 0) {
        return 1;
    }

    *out_buffer = malloc(size);
    return *out_buffer != NULL;
}

This pattern has useful properties:

  • The function validates the output location before dereferencing it.
  • It initializes *out_buffer to NULL, so failure does not leave an unspecified pointer behind.
  • The caller owns the allocation on success and must call free.

Linked-list modifications

Rather than handling the head node as a special case, code can use a pointer-to-pointer cursor:

void remove_value(struct Node **head, int value) {
    struct Node **link = head;

    while (*link != NULL) {
        if ((*link)->value == value) {
            struct Node *removed = *link;
            *link = removed->next;
            free(removed);
            return;
        }
        link = &(*link)->next;
    }
}

link always points to the pointer that leads to the current node: either head or a previous node’s next field. Therefore the same assignment, *link = removed->next, works for both head removal and middle removal.

Common Mistakes

Passing pointer instead of &pointer

If the parameter type is int **, pass the address of an int * variable.

int *number = NULL;
create_number(&number);  // Correct

This is wrong and should produce a compiler diagnostic:

create_number(number);   // Wrong: number has type int *, not int **

Dereferencing before checking for NULL

Both pointer levels may need validation.

void set_zero(int **out) {
    **out = 0;  // Unsafe if out or *out is NULL.
}

Check the levels needed by your operation:

if (out != NULL && *out != NULL) {
    **out = 0;
}

Forgetting that malloc can fail

Never write through an allocation result until you check it.

*result = malloc(sizeof **result);
if (*result == NULL) {
    return 0;
}

Confusing int ** with a two-dimensional array

These types are not interchangeable:

int matrix[3][4];

A 3 x 4 array is stored as one contiguous block and, when passed to a function, needs a parameter with the column size known:

void print_matrix(int rows, int matrix[][4]);

An int ** is instead a pointer to pointers, often used for separately allocated rows. Casting int (*)[4] or int[3][4] data to int ** is undefined behavior.

Using casts to hide incompatible pointer types

Do not force a type match with a cast such as (int **)matrix. Fix the declaration and function parameter so they describe the actual memory layout.

Losing an allocated pointer

If a function overwrites a pointer that already owns memory, the old allocation can become unreachable. Free or deliberately transfer ownership of the old allocation before replacing the pointer.

Comparisons

SituationSuitable typeWhy
Read or change one integerint *The function needs the integer’s address.
Allocate an integer and update the caller’s pointerint **The function must write a new address into the caller’s int *.
Traverse command-line stringschar **argvargv points to elements that are each char *.
Fixed-width contiguous 2D arrayint matrix[][COLS] or int (*matrix)[COLS]The compiler needs the row width to compute indexes.
Independently allocated rowsint **rowsEach row address is stored separately.

A helpful rule is: use one * for each level of object or pointer you must reach.

  • To modify a T through its address: use T *.
  • To modify a T * through its address: use T **.
  • To modify a T ** through its address: use T ***.

More indirection is not automatically better; use it only when the data structure or required update genuinely has another pointer level.

Take Quiz

Cheat Sheet

T value;
T *ptr = &value;       // Pointer to T
T **ptr_ptr = &ptr;    // Pointer to pointer to T

*ptr_ptr   // A T *
**ptr_ptr  // A T
void replace(T **out) {
    *out = /* a T * value */;
}

T *item = NULL;
replace(&item);

Key rules:

  • Use T ** when a function must modify a caller-owned T * variable.
  • Pass &pointer to a T ** parameter.
  • *out accesses the caller’s pointer; **out accesses the final object.
  • Check out != NULL before evaluating *out.
  • If allocating, check *out != NULL before dereferencing it.
  • A pointer-to-pointer is not automatically a 2D array.
  • Use free exactly once for each successful allocation, according to the ownership rules of the API.

FAQ

Why does C need pointers to pointers?

C function parameters are passed by value. A pointer to a pointer lets a function access and change the caller’s pointer variable.

Can I return a pointer instead of using T **?

Yes. For simple allocation, returning T * is often clear. A T ** output parameter is useful when the return value is needed for success or error status, or when an API follows an output-parameter convention.

Does **p always mean a pointer to a pointer?

In an expression, **p means dereference p twice. In a declaration, such as int **p, it declares p as a pointer to an int *.

Is char **argv a pointer to a pointer?

Yes. Each argv[i] is a char * pointing to a command-line string, and argv points to those char * elements.

Is int ** the same as int matrix[rows][cols]?

No. An int ** usually describes separately stored row pointers. A true multidimensional array is contiguous and has a different type and indexing layout.

Should I use a cast with malloc in C?

No. In C, assign malloc directly to the correctly typed pointer. A cast can hide a missing declaration for malloc and does not fix incorrect pointer types.

Can a pointer to a pointer be NULL?

Yes. A T ** can be NULL, and it can also point to a T * whose value is NULL. These are different states, so validate the level you will dereference.

Take Quiz

Mini Project

Description

Create a small singly linked-list utility that can add values at the front and remove a value. This demonstrates a practical reason for struct Node **: both operations may need to replace the list’s head pointer.

Goal

Build a list program that updates the head pointer safely through a pointer-to-pointer parameter.

Requirements

  1. Define a struct Node with an integer value and a next pointer.
  2. Write a function that prepends a value to the list.
  3. Write a function that removes the first node containing a requested value.
  4. Print the list before and after removal.
  5. Free every remaining node before the program exits.
Take Quiz

Keep learning

Related questions

View more