Extended Types of Pointers I

Learn about the concept of pointer to pointers and its diverse applications in programming.

In the previous lesson, we saw four basic types of pointers. There are some nontrivial types of pointers that every programmer should know and understand the usage of, including:

  1. Pointer to pointers

  2. void pointer

  3. Function pointers

Let’s start by discussing pointer to pointers and its applications.

Pointer to pointers

As we know, the pointer holds the address of any data type Type, and its generic syntax is:

(Type)* ptr = some_address;
// Example
int val;
int * ptr = &val;

But what if we want the pointer to hold an address of the pointer of the data type Type*? To store that address, we need to have a double pointer.

(Type*)* pptr = address_of_pointer;
// Example
int val;
int * ptr = &val;
int* * pptr = &ptr; // usually written as int** pptr; // `pptr` is just a name you may write any name

Let’s look at an example to see how to use pointer to pointers:

Press + to interact
#include <iostream>
using namespace std;
int main()
{
int val = 14;
int * ptr = & val;
cout <<"&val : "<<&val<<'\n';
cout <<" val : "<<val<<'\n';
cout <<"&ptr : "<<&ptr <<'\n';
cout <<" ptr : "<<ptr <<'\n';
cout <<"*ptr : "<<*ptr <<'\n';
/* int **ptr1 = &val this instruction is not posible the double pointer
must be pointed the memory of a pointer*/
int **pptr = &ptr;
cout<< "&pptr : "<<&pptr<<'\n';
cout<< " pptr : "<<pptr<<'\n';
cout<< "*pptr : "<< *pptr<<'\n';
cout<< "**ptr : " << **pptr << '\n';
return 0;
}
  • The code begins by declaring an integer variable val and initializing it with the value 14. Then, a pointer variable ptr is declared and assigned the memory address of val using the address-of operator (&). The subsequent lines print out the memory addresses and values of val, ptr, and *ptr. Next, a double pointer pptr is declared and assigned the memory address of ptr. The code then prints out the memory address of pptr, the value of pptr (which is the address of ptr), the value pointed to by pptr (which is the address of val), and the value pointed to by the double pointer (**pptr), which is the value of val.

In the following drawing, we can see what the memory structure for the above program looks like. The red arrow doesn’t exist physically (it is just shown to help you understand that it’s a pointer pointing to the memory location).

Press + to interact
Output of the program and its corresponding stack memory structure (because all 3 variables are local)
Output of the program and its corresponding stack memory structure (because all 3 variables are local)

Tip: When you run the above program, you might get different addresses each time. The above drawing is from when we ran the code while making this lesson.

Let’s look at a nontrivial example of how we can efficiently store upper and lower triangular matrices using the power of double pointers.

By utilizing dynamic memory allocation, pointers, and pointer to pointers, we can efficiently store an upper triangular matrix in memory. This approach allows us to store only the nonzero elements of the matrix and avoid storing certain zero elements, resulting in significant memory savings. Additionally, this approach offers a significant improvement over static two-dimensional arrays, which require each row to be of equal size. With dynamic memory allocation and pointer to pointers, we have the flexibility to store rows of different sizes, providing greater versatility and efficiency in managing memory for an upper triangular matrix.

Let’s write the code for storing a lower triangular matrix:

Press + to interact
#include <iostream>
using namespace std;
void printLowerTriangularMatrix(int** matrix, int n)
{
cout << "Lower Triangular Matrix:" << endl;
for (int i = 0; i < n; i++)
{
cout << "\t";
for (int j = 0; j < n; j++)
{
if(j>i)
cout << "0" << " ";
else
cout << matrix[i][j] << " ";
}
cout << endl;
}
}
void displayMemoryStructure(int** &matrix, int n)
{
cout << "&matrix: "<<&matrix
<< " => "<< matrix << endl;
// The pointers table
for(int i=0; i<n; i++) // &matrix[i] => matrix[i]
{
cout << "\t & matrix["<< i << "]: "<< matrix+i <<" => " << *(matrix+i) << " {";
for(int j=0; j<=i; j++)
{
cout << (*(matrix+i)+j) << ": "<<*(*(matrix+i)+j) ;
if(j!=i)
cout << " , ";
}
cout << " }" << endl;
}
}
int main()
{
int rows;
// cout << "Dimension: ";
cin >> rows;
// Dynamic memory allocation for rows
int** matrix = new int*[rows];
// Dynamic memory allocation for each row
for (int i = 0; i < rows; i++)
matrix[i] = new int[i + 1];
// Storing values in the lower triangular matrix
for (int i = 0; i < rows; i++)
for (int j = 0; j <= i; j++)
matrix[i][j] = rand()%10; // assigning a random value
// Printing the lower triangular matrix
printLowerTriangularMatrix(matrix, rows);
cout << "\n\nMemory Structure\n\n";
displayMemoryStructure(matrix, rows);
// Deallocating the memory
for (int i = 0; i < rows; i++)
delete[] matrix[i];
delete[] matrix;
return 0;
}

Enter the input below

Note: Enter the number of rows. For example: 3 , 5, or any other number.

  • This code demonstrates the creation and manipulation of a lower triangular matrix using dynamic memory allocation and pointers. The main focus is on memory structure and organization. A double pointer is created in the stack, which points to an array of pointers residing in the heap memory. Each pointer in the array then points to a one-dimensional array.

Visually, the memory structure can be represented as follows:

Press + to interact
Visual diagram of a lower triangular matrix in memory
Visual diagram of a lower triangular matrix in memory

Each pointer in the array points to a one-dimensional array of varying sizes. The memory addresses of the array elements have a difference of 8 bytes because each pointer takes up 8 bytes of memory. However, when looking at the one-dimensional arrays themselves, the increment factor is 4 bytes since they are arrays of integers.

Overall, this memory structure allows for efficient storage of a lower triangular matrix by dynamically allocating memory only for the required elements. Pointers enable flexibility in accessing and manipulating the matrix while optimizing memory usage.

Practice exercise: Upper triangular matrix

Modify the above code to store an upper triangular matrix. Carefully think about what should be changed. Challenge yourself by seeing if you can also write a program that displays the entire memory structure of the upper triangular matrix.

If you have trouble getting to the solution, you can click the “Solution” button to see how to solve the problem.

Exercise: Allocating higher dimensional dynamic arrays

This idea can be easily extended to make multidimensional arrays. As we saw earlier, when allocating a single-dimensional array, we needed a Type *. For allocating a two-dimensional array, we need a pointer to pointers Type**, which will point to an array of Type*.

Similarly, if we want to extend this idea to allocate a three-dimensional array, then we’ll need Type***. Let’s look at the following example to construct a cube of the dimension HHwhere it should have HH faces, each of H×HH \times H size.

Here's an example of a 3×33\times3 cube:

Press + to interact
Rubik's Cube with 3×3 faces
Rubik's Cube with 3×3 faces

Here’s the implementation, which allocates a 3D cube with each face made up of H×HH \times H letters. The first face should be made up of the character 'a', the next face be made up of the character 'b', and so on. Complete the functions highlighted in the code below.

If you have trouble getting to the solution, you can click the “Show solution” button to see how to solve the problem.

Press + to interact
#include <iostream>
using namespace std;
void AllocatingCube(char*** &cube, int size)
{ // Notice: cube pointer is passed by reference
// Write your code here
}
void initCube(char *** cube, int size)
{
// Write your code here
}
void printCube(char*** cube, int size)
{
// Write your code here
}
void deallocateCube(char*** &cube, int size)
{
// Write your code here
}
int main()
{
// This main should work
int H = 5; // Size of each dimension
char*** cube;
AllocatingCube(cube, H);
initCube(cube, H);
printCube(cube, H);
deallocateCube(cube, H);
return 0;
}

Expected output

Cube 5x5x5
Face 1:
A A A A A
A A A A A
A A A A A
A A A A A
A A A A A
Face 2:
B B B B B
B B B B B
B B B B B
B B B B B
B B B B B
Face 3:
C C C C C
C C C C C
C C C C C
C C C C C
C C C C C
Face 4:
D D D D D
D D D D D
D D D D D
D D D D D
D D D D D
Face 5:
E E E E E
E E E E E
E E E E E
E E E E E
E E E E E

Applications of pointer to pointer/pointers

Here are a few core applications:

  1. Dynamic memory allocation: Pointer to pointers is used for managing complex data structures and efficient memory allocation.

  2. Function parameters and return values: Pointer to pointers enables modifying variables and passing data between functions, allowing for flexible parameter passing and return values.

  3. Multidimensional arrays: Pointer to pointers facilitates the creation and manipulation of multidimensional arrays without the restrictions imposed by fixed-size multidimensional arrays, providing flexibility in array dimensions. Unlike fixed-size multidimensional arrays, passing multidimensional arrays allocated through a pointer to pointers does not require explicitly specifying each dimension’s size, avoiding wastage of memory and the need for separate functions for each dimension.

  4. Efficient memory management: Pointer to pointers allows for efficient memory management, as memory can be allocated and deallocated dynamically based on program needs.

These points highlight the key applications and advantages of using pointers to pointers in various scenarios.