Pointers to pointers and dynamic multidimensional arrays

Advanced note: This lesson is optional, for advanced readers who want to learn more about C++. No future lessons build on this lesson.

A pointer to a pointer is exactly what you'd expect: a pointer that holds the address of another pointer.

Pointers to pointers

A normal pointer to an int is declared using a single asterisk:

int* ptr;

A pointer to a pointer to an int is declared using two asterisks:

int** ptrptr;

A pointer to a pointer works just like a normal pointer -- you can dereference it to retrieve the value pointed to. And because that value is itself a pointer, you can dereference it again to get to the underlying value. These dereferences can be done consecutively:

int amount{ 42 };

int* ptr{ &amount };
std::cout << *ptr << '\n';

int** ptrptr{ &ptr };
std::cout << **ptrptr << '\n';

The above program prints:

42 42

Note that you can not set a pointer to a pointer directly to a value:

int amount{ 42 };
int** ptrptr{ &&amount };

This is because the address of operator (operator&) requires an lvalue, but &amount is an rvalue.

However, a pointer to a pointer can be set to null:

int** ptrptr{ nullptr };

Arrays of pointers

Pointers to pointers have a few uses. The most common use is to dynamically allocate an array of pointers:

int** grid{ new int*[8] };

This works just like a standard dynamically allocated array, except the array elements are of type "pointer to integer" instead of integer.

Two-dimensional dynamically allocated arrays

Another common use for pointers to pointers is to facilitate dynamically allocated multidimensional arrays (see the Multidimensional C-style Arrays lesson for a review of multidimensional arrays).

Unlike a two dimensional fixed array, which can easily be declared like this:

int grid[8][6];

Dynamically allocating a two-dimensional array is more challenging. You may be tempted to try something like this:

int** grid{ new int[8][6] };

But it won't work.

There are two possible solutions here. If the rightmost array dimension is constexpr, you can do this:

int rows{ 8 };
int (*grid)[6]{ new int[rows][6] };

The parentheses are required so that the compiler knows we want grid to be a pointer to an array of 6 int (which in this case is the first row of an 8-row multidimensional array). Without the parentheses, the compiler would interpret this as int* grid[6], which is an array of 6 int*.

This is a good place to use automatic type deduction:

int rows{ 8 };
auto grid{ new int[rows][6] };

Unfortunately, this relatively simple solution doesn't work if the rightmost array dimension isn't a compile-time constant. In that case, we have to get more complicated. First, we allocate an array of pointers (as per above). Then we iterate through the array of pointers and allocate a dynamic array for each array element. Our dynamic two-dimensional array is a dynamic one-dimensional array of dynamic one-dimensional arrays!

int** grid{ new int*[8] };
for (int row{ 0 }; row < 8; ++row)
    grid[row] = new int[6];

We can then access our array like usual:

grid[7][5] = 18;

With this method, because each array column is dynamically allocated independently, it's possible to make dynamically allocated two dimensional arrays that are not rectangular. For example, we can make a triangular array:

int** triangle{ new int*[8] };
for (int row{ 0 }; row < 8; ++row)
    triangle[row] = new int[row + 1];

In the above example, note that triangle[0] is an array of length 1, triangle[1] is an array of length 2, etc.

Deallocating a dynamically allocated two-dimensional array using this method requires a loop as well:

for (int row{ 0 }; row < 8; ++row)
    delete[] grid[row];
delete[] grid;

Note that we delete the array in the opposite order that we created it (elements first, then the array itself). If we delete grid before the array columns, then we'd have to access deallocated memory to delete the array columns. And that would result in undefined behavior.

Because allocating and deallocating two-dimensional arrays is complex and easy to mess up, it's often easier to "flatten" a two-dimensional array (of size rows by cols) into a one-dimensional array of size rows * cols:

int** grid{ new int*[8] };
for (int row{ 0 }; row < 8; ++row)
    grid[row] = new int[6];

int* flatGrid{ new int[48] };

Simple math can then be used to convert a row and column index for a rectangular two-dimensional array into a single index for a one-dimensional array:

int getIndex(int row, int col, int totalColumns)
{
     return (row * totalColumns) + col;
}

flatGrid[getIndex(7, 5, 6)] = 18;

Passing a pointer by address

Much like we can use a pointer parameter to change the actual value of the underlying argument passed in, we can pass a pointer to a pointer to a function and use that pointer to change the value of the pointer it points to (confused yet?).

However, if we want a function to be able to modify what a pointer argument points to, this is generally better done using a reference to a pointer instead. This is covered in the Pass by address lesson.

Pointer to a pointer to a pointer to...

It's also possible to declare a pointer to a pointer to a pointer:

int*** ptr3;

This can be used to dynamically allocate a three-dimensional array. However, doing so would require a loop inside a loop, and is extremely complicated to get correct.

You can even declare a pointer to a pointer to a pointer to a pointer:

int**** ptr4;

Or higher, if you wish.

However, in reality these don't see much use because it's not often you need so many levels of indirection.

Conclusion

We recommend avoiding using pointers to pointers unless no other options are available, because they're complicated to use and potentially dangerous. It's easy enough to dereference a null or dangling pointer with normal pointers -- it's doubly easy with a pointer to a pointer since you have to do a double-dereference to get to the underlying value!

Summary

Pointer to a pointer: Declared with two asterisks (int** ptrptr), holds the address of another pointer. Dereferencing twice (**ptrptr) accesses the underlying value.

Arrays of pointers: Pointers to pointers are commonly used to dynamically allocate arrays where each element is a pointer, declared as int** array{ new int*[size] }.

Dynamic 2D arrays with fixed rightmost dimension: If the rightmost dimension is constexpr, use int (*grid)[cols]{ new int[rows][cols] } or prefer auto grid{ new int[rows][cols] } for cleaner syntax.

Dynamic 2D arrays with variable dimensions: Allocate an array of pointers, then allocate a dynamic array for each pointer element. This allows non-rectangular (e.g., triangular) arrays.

Deallocation order: When deallocating dynamic 2D arrays created with pointers to pointers, delete the row arrays first (in a loop), then delete the array of pointers. Deleting in the wrong order causes undefined behavior from accessing deallocated memory.

Flattening alternative: Instead of using pointers to pointers, flatten a 2D array into a 1D array with Rows * Cols elements and use math to convert coordinates: index = (row * totalColumns) + col. This is simpler and less error-prone.

Passing pointers by address: While you can pass a pointer to a pointer to modify what the pointer points to, using a reference to a pointer is generally better.

Higher levels of indirection: Pointers to pointers to pointers (int***) and beyond are possible but rarely needed and increasingly complex.

Pointers to pointers add significant complexity and error potential. Prefer alternatives like flattening arrays, using standard library containers, or references to pointers when possible.