Pointer to Pointer (Double Pointers) in C
Updated 8 min read Aimerscrew
A pointer to pointer in C is exactly what the name says: a pointer that stores the address of another pointer. You've already seen that a pointer holds the address of a variable. A double pointer simply adds one more step to the chain. In this lesson, you'll learn how to declare one, how to follow it with two stars, and the two places where you'll really use it: changing a caller's pointer inside a function, and working with arrays of strings.
🌍 Real-life example
Picture a treasure hunt. The first clue doesn't hold the prize; it says "look under the doormat". Under the doormat you find a second note: "the prize is in the blue mailbox". Only then do you reach the treasure. A normal pointer is one note that leads straight to the treasure. A pointer to pointer is a note that tells you where to find another note.
What is a pointer to pointer in C?
A pointer to pointer (or double pointer) is a variable whose value is the address of a pointer variable. Since a pointer is itself a variable stored somewhere in memory, it has its own address, and that address can be stored in yet another pointer.
int score = 42; /* a normal int */
int *p = &score; /* p holds the address of score */
int **pp = &p; /* pp holds the address of p */
Read the declarations from the name outward: p is a pointer to an int, and pp is a pointer to a pointer to an int. Each star in the declaration adds one level. If you need a refresher on the basics, revisit pointers in C.
Two levels of dereference: *pp and **pp
To dereference a pointer means to follow it to whatever it points at, using the * operator. With a double pointer, you can follow one arrow or two:
| Expression | Type | What it gives you |
|---|---|---|
pp | int ** | The address of p |
*pp | int * | The value stored in p: the address of score |
**pp | int | The value of score itself: 42 |
*pp lands on p, **pp lands on score. (Addresses are made-up examples.)Here it is in a complete program. Notice that writing through **pp changes score, because two arrows lead right back to it:
#include <stdio.h>
int main(void) {
int score = 42;
int *p = &score;
int **pp = &p;
printf("score = %d\n", score);
printf("*p = %d\n", *p);
printf("**pp = %d\n", **pp);
**pp = 50; /* follow two arrows and store 50 */
printf("score is now %d\n", score);
return 0;
}
score = 42
*p = 42
**pp = 42
score is now 50
Checking where each pointer points
You could print the addresses with %p, but they change on every run. A clearer way to prove the chain is to compare the addresses directly with ==:
#include <stdio.h>
int main(void) {
int score = 42;
int *p = &score;
int **pp = &p;
printf("pp == &p ? %s\n", pp == &p ? "yes" : "no");
printf("*pp == &score ? %s\n", *pp == &score ? "yes" : "no");
printf("*pp == p ? %s\n", *pp == p ? "yes" : "no");
return 0;
}
pp == &p ? yes
*pp == &score ? yes
*pp == p ? yes
💡 Tip
Want to see the real addresses? Use printf("%p\n", (void *)pp);. On most 64-bit systems you'll see long hex numbers such as 0x7ffd3c2a1b40, and yours will differ every time you run the program.
Why would you need a double pointer?
You learned in pass by reference that a function needs the address of a variable to change it. The same rule applies when the variable you want to change is itself a pointer. To change an int, a function takes an int *. To change an int *, it takes an int **.
Here's a version that does not work:
void point_to_last(int *ptr, int values[], int count) {
ptr = &values[count - 1]; /* changes only the local copy */
}
The caller's pointer stays exactly where it was. The function received a copy of the pointer, moved the copy, and the copy disappeared when the function returned. (GCC with -Wall -Wextra even warns that the parameter is "set but not used".)
Changing a caller's pointer inside a function
Let's fix that with a real task: find the cheapest flight fare and make the caller's pointer point at it. The function receives &cheapest, so *best is the caller's pointer.
#include <stdio.h>
void find_cheapest(int prices[], int count, int **best) {
*best = &prices[0];
for (int i = 1; i < count; i++) {
if (prices[i] < **best) {
*best = &prices[i];
}
}
}
int main(void) {
int fares[5] = {320, 275, 410, 260, 300};
int *cheapest = NULL;
find_cheapest(fares, 5, &cheapest);
printf("Cheapest fare: $%d\n", *cheapest);
printf("It is flight number %d\n", (int)(cheapest - fares) + 1);
*cheapest = 240; /* apply a coupon to that fare */
printf("After coupon: $%d\n", fares[3]);
return 0;
}
Cheapest fare: $260
It is flight number 4
After coupon: $240
Inside the function, *best is the caller's pointer (type int *) and **best is the fare it currently points at (type int). Because cheapest now points into the array, changing *cheapest changes fares[3]. The subtraction cheapest - fares gives the index, as you saw in pointer arithmetic.
Swapping two pointers
The same idea lets you swap which strings two pointers refer to, without copying any characters:
#include <stdio.h>
void swap_names(const char **a, const char **b) {
const char *temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
const char *player_one = "Maya";
const char *player_two = "Leo";
swap_names(&player_one, &player_two);
printf("Player 1: %s\n", player_one);
printf("Player 2: %s\n", player_two);
return 0;
}
Player 1: Leo
Player 2: Maya
Only two addresses moved. The text "Maya" and "Leo" never changed places in memory.
Arrays of strings: char *names[]
An array of strings in C is usually an array of pointers, where each element points to the first character of one string. When such an array is passed around or walked with a pointer, its elements are reached through a double pointer.
#include <stdio.h>
int main(void) {
const char *playlist[] = {"Sunrise", "Ocean Drive", "Night Train"};
const char **song = playlist; /* points at playlist[0] */
printf("First song: %s\n", *song);
song++; /* move to the next pointer */
printf("Next song: %s\n", *song);
printf("First letter: %c\n", **song);
return 0;
}
First song: Sunrise
Next song: Ocean Drive
First letter: O
Here *song is a whole string (a const char *), and **song is its first character. We use const because string literals must never be modified.
How does argv look in memory?
When your program starts, main can receive the words typed after the program name as char *argv[], which is the same as char **argv. It's an array of string pointers that always ends with a NULL pointer. You'll use the real thing in command-line arguments; for now, here's a lookalike array that you can walk with a double pointer until you hit NULL:
#include <stdio.h>
int main(void) {
const char *args[] = {"./trip", "Paris", "Tokyo", "Lima", NULL};
for (const char **arg = args; *arg != NULL; arg++) {
printf("%s\n", *arg);
}
return 0;
}
./trip
Paris
Tokyo
Lima
If you ran a real program as ./trip Paris Tokyo Lima, its argv would have this exact shape: argv[0] is the program name, followed by each word, followed by NULL.
📝 Note
C allows even more levels, such as int ***ppp, but you'll almost never need more than two. If you find yourself writing three stars, there's usually a clearer design.
Common mistakes with double pointers
1. Passing the pointer instead of its address
If a function expects int **, call it with &cheapest, not cheapest. Passing cheapest makes GCC complain about an "incompatible pointer type" (a warning in older versions, an error since GCC 14), and if such code runs anyway it will likely crash.
2. Using one star too few or too many
printf("%d\n", *pp); /* wrong: *pp is an address, not an int */
GCC warns that %d expects an int but got int *. Count the arrows: you need **pp to reach the number.
3. Dereferencing before the inner pointer is set
In find_cheapest, reading **best before setting *best would follow the caller's NULL pointer and crash. Always point the inner pointer somewhere valid first.
Practice exercise
🏋️ Try it yourself
Write a function void pick_longer(const char *a, const char *b, const char **result) that makes *result point to the longer of the two names (use strlen from <string.h>). In main, call it with "Sam" and "Christopher" and print Longer name: Christopher.
Show solution
#include <stdio.h>
#include <string.h>
void pick_longer(const char *a, const char *b, const char **result) {
if (strlen(a) >= strlen(b)) {
*result = a;
} else {
*result = b;
}
}
int main(void) {
const char *winner = NULL;
pick_longer("Sam", "Christopher", &winner);
printf("Longer name: %s\n", winner);
return 0;
}
Longer name: Christopher
What's next?
Pointers can hold the address of data, and even of other pointers. They can also hold the address of code. Next, learn about function pointers in C, which let you store a function in a variable and call it later.
Key takeaways
- A pointer to pointer (double pointer) stores the address of another pointer, declared like int **pp.
- Each star follows one arrow: *pp gives the inner pointer, **pp gives the final value.
- To change an int inside a function you pass int *; to change an int * you pass int **.
- Passing a plain pointer only moves a local copy, so the caller's pointer stays the same.
- An array of strings is an array of pointers, and walking it uses a char ** (or const char **).
- argv in main is a char ** whose last element is NULL.
Frequently asked questions
What is a double pointer in C?
A double pointer is a pointer to a pointer: a variable that stores the address of another pointer variable. It is declared with two stars, for example int **pp = &p;.
What is the difference between *pp and **pp?
*pp follows one level and gives the pointer that pp points to, which is an address. **pp follows two levels and gives the actual value at the end of the chain, such as an int.
When should I use a pointer to pointer in C?
Use one when a function must change where a caller's pointer points, when working with arrays of strings such as argv, and later when building dynamic 2D arrays or linked lists.
Is char **argv the same as char *argv[]?
Yes. As a function parameter, char *argv[] is converted to char **argv, so both mean a pointer to the first element of an array of string pointers.
Can you have a triple pointer in C?
Yes, int ***ppp is valid C, and you can keep adding levels. In practice, more than two levels is rare and usually a sign that the code could be simpler.