Skip to main content

Double pointers

We've seen that a pointer is a variable that stores a memory address. as such, a pointer has itself an address in memory.

A double pointer is a pointer that stores the address of another pointer. It's also commonly called a "pointer to pointer".

This concept extends the indirection principle we've already seen with regular pointers, adding another layer of reference.

Declaration

To declare a double pointer, use two asterisks before the variable name, and similar to regular pointers, a double pointer needs to be initialized before use:

int x = 10;     // An integer variable
int *p = &x; // A pointer to x
int **pp = &p; // A double pointer to p
  • x contains the value 10
  • p contains the address of x (&x)
  • pp contains the address of p (&p)

Visualizing double pointers

The relationship between variables, pointers, and double pointers can be visualized like this:

Double pointer relationship

Fig.1. Relationship between a variable, a pointer, and a double pointer. Image courtesy of GeekforGeeks
Memory addresses vs. memory contents (at any pointer level)

Alternative diagram with text:

  &p=0xCAFEBABE        p=0xFEEDBEEF         *p=0xDEADBEEF      <-- memory address
┌──────────────┐ ┌───────────────┐ ┌────────────────┐
│ p=0xFEEDBEEF │ -> │ *p=0xDEADBEEF │ -> │ **p=0x01234567 │ <-- memory content
└──────────────┘ └───────────────┘ └────────────────┘
  • &p is the address of p, which is 0xCAFEBABE. The memory location 0xCAFEBABE stores the value of p, which is 0xFEEDBEEF. That is also the address of *p.
  • The value of p is the address of *p.
  • The value of &p is the address of p.
  • The value of *p is the address of **p.

And so on. Note that * and & are like opposites, and *&p == p == &*p. From here.

Dereferencing double pointers

With double pointers, you can dereference once or twice:

  • *pp gives the value of p (which is the address of x)
  • **pp gives the value that p points to (which is the value of x)
int x = 10;
int *p = &x;
int **pp = &p;

printf("Value of x: %d\n", x); // Prints 10
printf("Value of *p: %d\n", *p); // Prints 10
printf("Value of *pp: %p\n", *pp); // Prints the address of x
printf("Value of **pp: %d\n", **pp); // Prints 10

// We can modify x through pp
**pp = 20;
printf("New value of x: %d\n", x); // Prints 20
Value of x: 10
Value of *p: 10
Value of *pp: 0xADDRESS
Value of **pp: 10
New value of x: 20

In this example, all three expressions x, *p, and **pp refer to the same memory location, just with different levels of indirection.

Chain of references

Think of double pointers as a chain of references:

#include <stdio.h>

int main() {
int value = 42;
int *ptr = &value; // Points to value
int **dptr = &ptr; // Points to ptr

printf("Direct access: %d\n", value);
printf("Through pointer: %d\n", *ptr);
printf("Through double pointer: %d\n", **dptr);

// Change value through double pointer
**dptr = 100;
printf("New value: %d\n", value);

return 0;
}
Direct access: 42
Through pointer: 42
Through double pointer: 42
New value: 100

Double pointers in functions

One important use of double pointers is when you need to modify a pointer inside a function:

void allocate_int_pointer(int **pp) {
// Create a new int and make the original pointer point to it
int *temp = (int*)malloc(sizeof(int));
*temp = 42;
*pp = temp; // Modify where the original pointer points
}

int main() {
int *p = NULL;
allocate_int_pointer(&p); // Pass address of the pointer

printf("Value: %d\n", *p); // Prints 42
free(p); // Don't forget to free
return 0;
}
caution

Without using a double pointer, we couldn't modify where the original pointer p points to from within the function. This is because function parameters are passed by value in C, even when those parameters are pointers.

Practical use cases

Double pointers are commonly used for:

  1. Modifying pointer values in functions (as shown above)

  2. Arrays of strings (which are essentially arrays of character pointers):

char *names[] = {"Alice", "Bob", "Charlie"};
char **ptr = names; // ptr is a pointer to the first string pointer

printf("%s\n", *ptr); // Prints "Alice"
printf("%s\n", *(ptr+1)); // Prints "Bob"
  1. Building data structures like linked lists where you need to modify head pointers

  2. Creating 2D arrays dynamically

tip

Many standard C library functions that need to modify string pointers use double pointers. For example, sorting an array of strings might use this pattern.

Common mistake: Mixing indirection levels

A common mistake is mixing up indirection levels:

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

printf("%d\n", **pp); // Correct: 10
printf("%d\n", *pp); // Wrong! This is an address, not an int

Remember that each level of indirection (*) drops you down one level in the reference chain.

Sizing double pointers

Like all pointers, the size of a double pointer depends on your system architecture (typically 4 bytes on 32-bit systems and 8 bytes on 64-bit systems):

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

On a 64-bit system, this would print:

Size of int: 4 bytes
Size of int*: 8 bytes
Size of int**: 8 bytes

Notice that both single and double pointers have the same size since they both store addresses.

Multilevel pointers

C doesn't limit you to just double pointers. You can create pointers with even more levels of indirection:

int x = 10;
int *p = &x; // Single pointer
int **pp = &p; // Double pointer
int ***ppp = &pp; // Triple pointer!

printf("%d\n", ***ppp); // Prints 10
danger

While C allows any level of pointer indirection, using more than two levels is rare in practice. As indirection increases, code becomes harder to understand and maintain. Triple or higher-level pointers are usually a sign that you might want to reconsider your design.

Double pointers vs. multi-dimensional arrays

It's important not to confuse double pointers with 2D arrays, even though they can sometimes be used interchangeably.

A 2D array has contiguous memory allocation, while a double pointer can point to arrays that are scattered through memory.

For example:

int matrix[3][4];        // True 2D array - contiguous memory
int **dynamic_matrix; // Double pointer - can be used for 2D array

We'll explore this relationship further when we discuss dynamic memory allocation.