Pointers to Pointers

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;ptr stores the address of a (a normal pointer, one asterisk)
  • int **ptrTwo = &ptr;ptrTwo stores the address of ptr (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 ─

  • ptrTwo by itself ─ the address stored inside ptrTwo, which is the address of ptr (2000).
  • *ptrTwo ─ one hop! We follow ptrTwo to ptr, and get what ptr holds ─ the address of a (1000). So *ptrTwo is really the same as ptr.
  • **ptrTwo ─ two hops! We follow all the way down to a and 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(&current, &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 &current (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 levelint** points to int*, which points to int.
  • Each * when using the pointer takes one hop closer ─ **pp reaches the final value.
  • **pp is 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 &ptr as an int**, 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** or char**).
  • An int** must point to a real int* ─ 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 **argv in main is 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 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *