pointers-memory Lesson 4 20 min read

What Are Double Pointers and When Do I Use Them?

Pointers to pointers — modifying pointers from inside functions

Reading: C Text: Ch. 6 §1 (advanced pointer topics), Ch. 13 (Dynamic Data Structures)

After this lesson, you will be able to:

  • Explain why modifying a caller’s pointer requires an int ** parameter
  • Write a function that allocates memory via calloc and returns it through a double pointer
  • Trace code involving two levels of indirection (**pp) using memory diagrams
  • Compare the return-the-pointer vs. double-pointer-parameter approaches

When One Star Isn’t Enough

You know that to modify an int from inside a function, you pass an int *. But what if you want to modify a pointer from inside a function? You need a pointer to that pointer — an int **.

This comes up when a function needs to allocate memory and hand back the pointer, or when you’re building data structures like linked lists where you modify where a pointer points.


Pointers to Pointers

The Pattern

To modify a variable of type T from inside a function, pass a T *. Apply this rule recursively:

Want to modify Pass Function parameter
int x &x int *p
int *p &p int **pp

A Concrete Example

void allocate_array(int **arr_ptr, int size)
{
    *arr_ptr = calloc(size, sizeof(int));    // Modifies caller's pointer
}

int main(void)
{
    int *data = NULL;
    allocate_array(&data, 10);    // Pass address of the pointer

    // data now points to allocated memory
    data[0] = 42;
    printf("%d\n", data[0]);      // 42

    free(data);
    return 0;
}

Memory diagram:

  main:                         allocate_array:
  ┌──────┐                     ┌─────────┐
  │ data │ ──→ [heap memory]   │ arr_ptr │ ──→ data (in main)
  └──────┘                     └─────────┘

  *arr_ptr = calloc(...)  →  changes where 'data' points

Why Single Pointer Doesn’t Work

void broken_allocate(int *arr, int size)
{
    arr = calloc(size, sizeof(int));    // Changes LOCAL copy of pointer
    // Caller's pointer is unchanged!
}

This modifies the function’s copy of the pointer — the caller’s pointer stays NULL.

Key Insight: Just as *p = value modifies what p points to, *pp = address modifies what pp points to. If pp is a int ** pointing to a int *, then *pp accesses that int * — and you can change where it points.

Check Your Understanding
A function needs to allocate an array and give the caller the pointer. Which signature works?
A void alloc(int *arr, int n) with arr = calloc(n, sizeof(int))
B void alloc(int arr[], int n) with arr = calloc(n, sizeof(int))
C void alloc(int *arr, int n) with *arr = calloc(n, sizeof(int))
D void alloc(int **arr, int n) with *arr = calloc(n, sizeof(int))
Answer: D. To modify the caller's pointer, you need a pointer to it — int **arr. Then *arr = calloc(...) writes the new address into the caller's variable. Options A and B change only the local copy. Option C has a type mismatch — *arr with int *arr gives an int, and you can't assign a pointer to an int.

Reading Double Pointer Code

Trace through this step by step:

Before you read further: look at the code below and predict what each printf outputs. After **pp = 99, what is the value of x? Draw the pointer chain on paper before reading the answer.

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

printf("%d\n", x);        // 5
printf("%d\n", *p);       // 5 (follow one arrow)
printf("%d\n", **pp);     // 5 (follow two arrows)

**pp = 99;                 // Changes x through two levels
printf("%d\n", x);        // 99
  ┌────┐     ┌───┐     ┌───┐
  │ pp │ ──→ │ p │ ──→ │ x │ = 99
  └────┘     └───┘     └───┘

When You’ll Use Double Pointers

  1. Functions that allocate and return memory (shown above)
  2. Linked list operations that modify the head pointer (Series 4)
  3. Arrays of stringschar **argv is a pointer to an array of char *

From Java: Java doesn’t have double pointers because references handle this automatically. When a Java method creates an object and assigns it to a parameter, the caller’s reference is unchanged — exactly the same problem. Java works around it by returning the new object. C gives you the choice: return the pointer, or use a double pointer parameter.

Deep dive: Return the pointer vs. double pointer — when to use each

There are two ways to give a caller a pointer to newly allocated memory:

Approach 1: Return the pointer (simpler, preferred when possible)

int *create_array(int size, int value)
{
    int *arr = calloc(size, sizeof(int));
    for (int i = 0; i < size; i++) arr[i] = value;
    return arr;
}

// Caller:
int *data = create_array(5, 42);

Approach 2: Double pointer parameter (needed in specific cases)

void create_array(int **arr_ptr, int size, int value)
{
    *arr_ptr = calloc(size, sizeof(int));
    for (int i = 0; i < size; i++) (*arr_ptr)[i] = value;
}

// Caller:
int *data = NULL;
create_array(&data, 5, 42);

When to use which:

Situation Use
Function creates one pointer Return it — simpler and clearer
Function needs to modify multiple pointers Double pointer parameters — can’t return two things
Return value is already used for an error code Double pointer for the data, return int for success/failure
Linked list insertion at head Double pointer to head — *head = new_node modifies the list

Lab 6 uses the return approach (array = addItem(&length, array)). You’ll see double pointers more in data structure code where functions modify where list/tree pointers point.

Deep dive: Double pointers and linked list insertion

The classic use case for double pointers is inserting at the head of a linked list:

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

// WITHOUT double pointer — broken for head insertion:
void insert_front_broken(Node *head, int value)
{
    Node *new = calloc(1, sizeof(Node));
    new->data = value;
    new->next = head;
    head = new;       // Changes local copy — caller's head unchanged!
}

// WITH double pointer — works:
void insert_front(Node **head_ptr, int value)
{
    Node *new = calloc(1, sizeof(Node));
    new->data = value;
    new->next = *head_ptr;
    *head_ptr = new;   // Changes caller's head pointer
}

// Caller:
Node *head = NULL;
insert_front(&head, 10);    // head now points to node with 10
insert_front(&head, 20);    // head now points to 20 → 10 → NULL

Without the double pointer, the head of the list can never change — you’d always be stuck with the original head node. This is why every linked list implementation in C uses Node ** for functions that might modify the head.

Check Your Understanding
Given: int x = 5; int *p = &x; int **pp = &p;
Which expression does NOT evaluate to 5?
A x
B *p
C *pp
D **pp
Answer: C. *pp dereferences once — it follows pp to get p, which is an address (like 0x1000), not the value 5. You need **pp to follow both arrows and reach x. This is the core of double pointers: one star gets you the inner pointer, two stars get you the final value.
Why does this matter?

Double pointers appear everywhere in real C: linked list head modifications, dynamic array resizing, argv in main, and any function that allocates memory on behalf of the caller. If you can trace two levels of indirection, you can read most C library code.

Quick Check: Why does arr = calloc(...) inside a function fail to modify the caller's pointer?

C is pass-by-value. The function gets a copy of the pointer. arr = calloc(...) changes the copy to point to new memory, but the caller’s original pointer is unchanged. To modify the caller’s pointer, pass int **arr_ptr and write *arr_ptr = calloc(...).

Quick Check: What does **pp = 42 do?

It follows two levels of indirection: *pp accesses the pointer that pp points to, and **pp accesses the value that that pointer points to. It writes 42 to the final destination.

Quick Check: What type is argv in int main(int argc, char *argv[])?

It’s equivalent to char **argv — a pointer to an array of char * (string pointers). argv[0] is the first string (program name), which is a char *.


The Rule of Stars

The pattern is clean: to modify a T from inside a function, pass a T *. It doesn’t matter what T is — int, int *, struct Node * — the rule applies uniformly. Double pointers feel intimidating, but they’re just the same pattern applied twice.

Next: a pointer review to solidify everything before moving to dynamic memory allocation. You’ll practice tracing pointer code and draw memory diagrams for increasingly complex examples.

Big Picture: Double pointers are the last piece of the pointer puzzle. With single pointers you can modify values. With double pointers you can modify pointers themselves. This is what makes linked lists, trees, and dynamic data structures possible in C. Series 4 will use double pointers heavily when you build linked lists.