Hi everyone ✋
In the previous post we learned about smart pointers ─ the modern, safe way to handle dynamic memory. At the very end, I promised we’d look at something that sounds a little dizzying ─ pointers to pointers 😵💫
If you haven’t read the earlier posts in this series, please go through them first ─ especially the very first post, because today we’re just taking the same idea we started with and stacking it one level higher.
So, can a pointer really point at another pointer? Yes! And once we see how, the mysterious double asterisk ** stops being scary and starts making perfect sense 🥳
Let’s take a deep dive 🦴
A quick refresher first
Let’s remember the core idea from our very first post. A pointer is just a variable that stores the address of another variable.
int a = 15; int *ptr = &a; // ptr holds the address of a
Here ptr is a variable too ─ and it lives somewhere in memory, just like a does. So ptr also has its own address 🤔
And that innocent little observation is the whole key to today’s topic. If ptr has an address… then we can store that address in another pointer 😲
The big idea ─ a pointer that points at a pointer
A pointer to a pointer is exactly what it sounds like ─ a pointer whose value is the address of another pointer. We declare it with two asterisks **.
#include <iostream>
using namespace std;
int main()
{
int a = 15;
int *ptr = &a; // ptr points to a
int **ptrTwo = &ptr; // ptrTwo points to ptr
cout << "value of a: " << a << endl;
cout << "ptr points to value: " << *ptr << endl;
cout << "ptrTwo points to value: " << **ptrTwo << endl;
return 0;
}
The output will be ─
value of a: 15 ptr points to value: 15 ptrTwo points to value: 15
🥳🥳 All three reached the same 15, just through different numbers of “hops.”
Let’s read the two declarations carefully ─
int *ptr = &a;─ptrstores the address ofa(a normal pointer, one asterisk)int **ptrTwo = &ptr;─ptrTwostores the address ofptr(a pointer to a pointer, two asterisks)
Notice the beautiful pattern ─ each * in the type means “one more level of pointing.” An int* points to an int. An int** points to an int*. The rule just repeats 🥳
Seeing it in memory
This is the kind of topic that really clicks once we draw it out. Let’s picture the three variables in memory, each in its own box with its own address.
Say a lives at address 1000, ptr lives at 2000, and ptrTwo lives at 3000.
| Variable | Stored value | Its own address |
|---|---|---|
| a | 15 | 1000 |
| ptr | 1000 (address of a) | 2000 |
| ptrTwo | 2000 (address of ptr) | 3000 |
Read it slowly and the chain appears 👀 ─ ptrTwo holds 2000, which is where ptr lives. And ptr holds 1000, which is where a lives. So starting from ptrTwo, we can hop all the way down to a, one address at a time.
It’s like a treasure hunt 🗺️ ─ ptrTwo is a note that says “the next clue is at 2000.” We go to 2000 and find ptr, another note saying “the treasure is at 1000.” We go to 1000 and finally find our treasure, a, holding 15.
Dereferencing step by step ─ the heart of it all
Now for the most important part of the whole post ─ how do we follow that chain? With the dereference operator * from our very first post. Each * takes us one hop closer.
Let’s use ptrTwo and see what each level of dereferencing gives us.
#include <iostream>
using namespace std;
int main()
{
int a = 15;
int *ptr = &a;
int **ptrTwo = &ptr;
cout << "ptrTwo : " << ptrTwo << endl; // address of ptr
cout << "*ptrTwo : " << *ptrTwo << endl; // address of a
cout << "**ptrTwo : " << **ptrTwo << endl; // the value 15
return 0;
}
The output will be something like ─
ptrTwo : 0x7ffe...2000 *ptrTwo : 0x7ffe...1000 **ptrTwo : 15
🥳 Let’s walk through each line, because this is the core skill ─
ptrTwoby itself ─ the address stored insideptrTwo, which is the address ofptr(2000).*ptrTwo─ one hop! We followptrTwotoptr, and get whatptrholds ─ the address ofa(1000). So*ptrTwois really the same asptr.**ptrTwo─ two hops! We follow all the way down toaand get its value,15.
Here’s a neat way to see the relationships ─
| Expression | Same as | Result |
|---|---|---|
**ptrTwo |
*ptr |
a’s value → 15 |
*ptrTwo |
ptr |
address of a |
ptrTwo |
&ptr |
address of ptr |
Each * peels away one layer, like unwrapping a gift inside a gift 🎁 The number of asterisks we use tells us how many hops we take.
Changing the original through two levels
Remember from the pass-by-reference post how a pointer lets us change the original variable? A pointer to a pointer can do the exact same thing ─ we just need to dereference all the way down first.
Since **ptrTwo reaches a itself, assigning to it changes a directly 🥳
#include <iostream>
using namespace std;
int main()
{
int a = 15;
int *ptr = &a;
int **ptrTwo = &ptr;
cout << "before: a = " << a << endl;
**ptrTwo = 500; // reach all the way down and change a
cout << "after: a = " << a << endl;
return 0;
}
The output will be ─
before: a = 15 after: a = 500
🥳🥳 We changed a to 500 without ever touching a directly ─ we went through ptrTwo, down to ptr, down to a, and set the value there.
This works for exactly the reason we learned in the pass-by-reference post ─ **ptrTwo is not a copy, it is a. Writing to it writes straight to a‘s memory. The two hops just took us to the right place first 🤔
Can we go even deeper? Pointer to pointer to pointer 🤯
You might be wondering ─ “if two levels work, can we go three? Or more?” Yes! The pattern just keeps repeating. Each extra level adds one more *.
#include <iostream>
using namespace std;
int main()
{
int a = 15;
int *ptr = &a; // level 1
int **ptrTwo = &ptr; // level 2
int ***ptrThree = &ptrTwo; // level 3!
cout << "value of a: " << a << endl;
// three hops all the way down to a
cout << "triple dereference: " << ***ptrThree << endl;
// and we can still change a from way up here
***ptrThree = 500;
cout << "value of a now: " << a << endl;
return 0;
}
The output will be ─
value of a: 15 triple dereference: 500 value of a now: 500
🥳 Three asterisks, three hops. int ***ptrThree is “a pointer to a pointer to a pointer to an int.” And ***ptrThree follows the chain all three steps down to a.
In theory this can go on forever ─ four levels, five levels, and so on. But in real code, going beyond two levels is very rare 🤔 Three or more quickly becomes confusing and is almost always a sign there’s a cleaner design. So it’s great to understand the pattern, but in practice, we mostly stop at **.
But why would we ever use this? 🤔
This is the natural question ─ pointers to pointers look clever, but where are they actually useful? Let’s look at real problems, because that’s where the advantage really shows.
Real problem 1 ─ a function that can’t update our pointer 😱
Let’s start with a problem that genuinely trips people up. Imagine we’re writing a program that manages a “current player” in a game. We have a pointer to whichever player is active, and we want a function switchPlayer that repoints it to a new player.
Here’s a first, natural-looking attempt ─
#include <iostream>
using namespace std;
// attempt 1 ─ take the pointer by value
void switchPlayer(int *current, int *newPlayer)
{
current = newPlayer; // point current at the new player
}
int main()
{
int player1 = 100;
int player2 = 200;
int *current = &player1; // start on player1
cout << "before: current score = " << *current << endl;
switchPlayer(current, &player2); // try to switch to player2
cout << "after: current score = " << *current << endl;
return 0;
}
We expect current to now point at player2. But the output is ─
before: current score = 100 after: current score = 100
😱 Nothing changed! current is still pointing at player1. Why?
Here’s the trap, and it’s the exact same lesson from the pass-by-value post ─ when we passed current into the function, the function got a copy of the pointer. Yes, it’s a copy of an address, but it’s still a copy. Inside switchPlayer, the line current = newPlayer; only changed that local copy. The original current back in main never budged.
Think about it this way ─ to change an int, we needed its address (an int*). So to change a pointer, we need its address ─ and the address of an int* is an int** 🤔
The solution ─ pass a pointer to the pointer 🥳
Let’s fix it by passing the address of current, so the function can reach the real pointer, not a copy.
#include <iostream>
using namespace std;
// solution ─ take a pointer to the pointer
void switchPlayer(int **current, int *newPlayer)
{
*current = newPlayer; // change what the ORIGINAL pointer points to
}
int main()
{
int player1 = 100;
int player2 = 200;
int *current = &player1;
cout << "before: current score = " << *current << endl;
switchPlayer(¤t, &player2); // pass the ADDRESS of current
cout << "after: current score = " << *current << endl;
return 0;
}
Now the output is ─
before: current score = 100 after: current score = 200
🥳🥳 It works! By passing ¤t (an int**), the function received the real address of our pointer. Inside, *current = newPlayer; did one hop up to the original pointer and repointed it. The change stuck.
This is the classic reason pointers to pointers exist ─ whenever a function needs to modify a caller’s pointer (not just the value it points to), we pass a pointer to that pointer. You’ll see this pattern all over C code, especially in functions that allocate memory for you, like this ─
#include <iostream>
using namespace std;
// a function that allocates and hands back a new array through the pointer
void makeArray(int **out, int size)
{
*out = new int[size]; // set the caller's pointer to fresh memory
}
int main()
{
int *data = nullptr;
makeArray(&data, 5); // data now points to a real array!
data[0] = 42;
cout << data[0] << endl;
delete[] data;
return 0;
}
The output will be ─
42
🥳 Because we passed &data, the function could reach back and set our pointer to freshly allocated memory. Without the int**, data would still be nullptr after the call. This “allocate through an out-parameter” pattern is extremely common in real libraries 🤔
Real problem 2 ─ dynamic 2D data sized at run time
Remember the arrays post, where we built a flexible 2D grid using an array of pointers? That whole idea is a pointer to a pointer 🥳
When we have an array of int* (each pointing to a row), the array name decays into an int** ─ a pointer to the first int*. This is the classic way to build a dynamic 2D array whose dimensions are decided at run time ─ something a normal fixed array simply cannot do.
#include <iostream>
using namespace std;
int main()
{
int rows = 2;
int cols = 3;
// an array of int pointers ─ this is our int**
int **grid = new int*[rows];
// each element points to its own row (an int array)
for (int i = 0; i < rows; i++)
{
grid[i] = new int[cols];
}
// fill and use it just like grid[i][j]!
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
grid[i][j] = (i + 1) * (j + 1);
cout << grid[i][j] << " ";
}
cout << endl;
}
// clean up ─ remember the dynamic memory post!
for (int i = 0; i < rows; i++)
{
delete[] grid[i]; // free each row
}
delete[] grid; // free the array of pointers
return 0;
}
The output will be ─
1 2 3 2 4 6
🥳🥳🥳 A fully dynamic 2D grid, sized at run time, built entirely from a pointer to a pointer!
Look how everything from the series came together here ─ we used new/delete[] from the dynamic memory post, the array-of-pointers idea from the arrays post, and the ** from today. And notice the cleanup carefully ─ because we allocated in two layers (the rows, then the array of pointers), we must delete[] in two layers too, in the reverse order. Every new[] still needs its matching delete[] 🤔
(In modern C++, we’d usually reach for a vector<vector<int>> or a smart pointer instead ─ but understanding the raw int** version is exactly what makes those conveniences make sense.)
Real problem 3 ─ a list of names that can grow 📝
Let’s do one more real-world example, because this is where pointers to pointers shine in everyday programs. Imagine we’re storing a list of student names. Each name is a C-style string ─ a char* (remember the strings post!). A whole list of names is therefore an array of char*… which is a char** 🥳
#include <iostream>
using namespace std;
// print every name in the list
void printNames(char **names, int count)
{
for (int i = 0; i < count; i++)
{
cout << (i + 1) << ". " << names[i] << endl;
}
}
int main()
{
// an array of char pointers ─ each points to a string
char *names[3];
names[0] = (char *)"imran";
names[1] = (char *)"soudha";
names[2] = (char *)"anik";
// names decays to char** when passed
printNames(names, 3);
return 0;
}
The output will be ─
1. imran 2. soudha 3. anik
🥳 Look at the two layers working together. names is an array of char* ─ each element points to the first character of one name (from the strings post). When we pass names to the function, it decays into a char** ─ a pointer to the first char*. So inside printNames, the parameter char **names is receiving exactly that.
Then names[i] gives us one char* (a single string), and cout prints the whole string thanks to the special char* behavior we learned. Two levels of pointers, working exactly as designed 🤔
In fact, you’ve seen this char** before without realizing it! Every C++ program’s main can be written as ─
int main(int argc, char **argv)
That char **argv is a list of the command-line arguments passed to your program ─ an array of strings, i.e. a pointer to a pointer to char. So the very entry point of C++ programs uses the idea we learned today 🥳
The type must match exactly ⚠️
Here’s a subtle but important aspect ─ a pointer to a pointer is picky about its type, just like every pointer we’ve met. An int** can only hold the address of an int*, never the address of a plain int.
int a = 15; int **pp = &a; // ERROR ─ won't compile! 😱
This fails because &a is an int* (the address of an int), but pp wants an int** (the address of an int*). The levels don’t match ─ we’re off by one. The compiler catches this immediately, which is a good thing 🥳
The fix is to go through a proper middle pointer ─
int a = 15; int *ptr = &a; // ptr is an int* int **pp = &ptr; // &ptr is an int** ─ now it matches! 🥳
There always has to be a real pointer in the middle. We can’t skip a level. This is why a pointer to a pointer always comes as a chain ─ each link must exist for the next one to point at it 🤔
Also worth knowing ─ what happens if a middle pointer is nullptr? Then dereferencing too far will crash, just like any null dereference ─
int **pp = nullptr; cout << **pp; // DANGER ─ following a null chain! 😱
Always be sure every link in the chain actually points somewhere valid before you hop down it. This connects straight to the null-pointer safety we’ll explore in a later post 😉
A common confusion ─ how many stars? ⚠️
Beginners often get tangled up counting asterisks, so here’s a simple rule to keep it straight.
The number of * in the declaration tells you how many levels deep the pointer is ─
int a─ a plain int (zero levels)int *p─ points to an int (one level)int **pp─ points to an int* (two levels)int ***ppp─ points to an int** (three levels)
And when using it, the number of * you apply tells you how many hops you take toward the value. To reach the final int, you must apply exactly as many * as the declaration has.
So for int **pp, you need **pp (two stars) to reach the actual value. Use too few, and you’ll get an address instead of the value; that mismatch is the source of most pointer-to-pointer bugs 🤔
So what should we remember? 🤔
Let’s wrap up the key takeaways ─
- A pointer is a variable, so it has its own address ─ which means another pointer can point at it.
- A pointer to a pointer is declared with two asterisks (
int **pp), and holds the address of a pointer. - Each
*means one more level ─int**points toint*, which points toint. - Each
*when using the pointer takes one hop closer ─**ppreaches the final value. **ppis the original variable, so writing to it changes the original (just like pass-by-reference, two levels deep).- You can chain further (
***, and beyond), but two levels is almost always the practical limit. - A key real use is changing a pointer inside a function ─ pass
&ptras anint**, including the common “allocate memory for you” out-parameter pattern. - Another is dynamic 2D arrays and lists of strings (an array of pointers, which decays to
int**orchar**). - An
int**must point to a realint*─ the type must match exactly, and there’s always a pointer in the middle of the chain. - Every C++ program already uses a pointer to a pointer ─ the
char **argvinmainis a list of command-line arguments. - Never hop down a chain where a middle link is
nullptr─ that’s an undefined-behavior crash. - Count carefully ─ the asterisks in the declaration must match the asterisks you use to reach the value.
Once these click, that intimidating ** becomes just a natural extension of everything we already knew 👀✨
Congratulations 🥳🥳🥳
We now understand pointers to pointers ─ one of the ideas that makes many people give up on pointers, and one you’ve just conquered.
In the next post we will talk about void pointers ─ the mysterious “typeless” pointer we’ve bumped into a couple of times already, and why it can point at anything 😉
Happy Coding 💻 🎵