Pointer to Pointer (Double Pointer)

This chapter delves into the fascinating world of pointers to pointers in C++. You'll learn how to create a chain of indirection, essentially pointing to a variable that stores the memory address of another pointer! Buckle up for an in-depth exploration, from fundamental concepts to practical applications.

Single Pointers: A Recap

Before diving into pointers to pointers, let’s solidify our understanding of single pointers. A pointer is a variable that stores the memory address of another variable. It acts like a label pointing to a specific location in memory where the actual data resides.

				
					int num = 42;
int* ptr = # // ptr stores the memory address of num

				
			

Introducing Pointers to Pointers

A pointer to a pointer takes the concept a step further. It’s a variable that stores the memory address of another pointer variable. In essence, it’s a pointer that points to a pointer!

				
					int num = 42;
int* ptr = # // ptr points to num

int** ptr_ptr = &ptr; // ptr_ptr points to the memory address of ptr (which points to num)

				
			

Declaration Syntax

Declaring a pointer to a pointer involves adding an extra asterisk (*) to the original pointer declaration syntax. Here’s the format:

				
					data_type** pointer_name;

				
			
  • data_type: The data type that the pointer ultimately points to (e.g., int, char, std::string).
  • pointer_name: The name you choose for your pointer to pointer variable.

Understanding Indirection with Pointers to Pointers

The key concept to grasp is indirection. When you use a single pointer, you use the dereference operator (*) to access the value stored at the memory address it points to. With pointers to pointers, you need to dereference twice to reach the actual data.

				
					int num = 42;
int* ptr = # // ptr points to num (value: 42)
int** ptr_ptr = &ptr; // ptr_ptr points to ptr (address of num)

std::cout << num << std::endl; // Accesses num directly (output: 42)
std::cout << *ptr << std::endl; // Dereferences ptr to access value at its address (output: 42)

// Accessing value with pointer to pointer (double dereference)
std::cout << **ptr_ptr << std::endl; // Dereferences ptr_ptr to get ptr's value (address of num), then dereferences ptr to get the actual value at that address (output: 42)

				
			

Explanation:

  1. num stores the value 42.
  2. ptr is a regular pointer that points to num.
  3. ptr_ptr is a pointer to a pointer that points to ptr.
  4. The first std::cout directly accesses num and prints its value (42).
  5. The second std::cout dereferences ptr using *ptr to access the value at the memory address stored in ptr (which is the address of num), and prints it (42).
  6. The third std::cout demonstrates double dereference. We dereference ptr_ptr with **ptr_ptr to get the value stored at the memory address it points to (which is the address of num). Then, we dereference the resulting pointer (ptr) using * to access the final value at num (42).

Applications of Pointers to Pointers

The key concept to grasp is indirection. When you use a single pointer, you use the dereference operator (*) to access the value stored at the memory address it points to. With pointers to pointers, you need to dereference twice to reach the actual data.

Dynamic Memory Allocation with Two-Dimensional Arrays

C++ doesn’t support built-in two-dimensional arrays. However, you can simulate them using pointers to pointers. Here’s how:

				
					#include <iostream>

int main() {
    int rows = 3, cols = 4;

    // Allocate memory for rows (array of pointers to int)
    int** matrix = new int*[rows];

    // Allocate memory for each row (array of integers)
    for (int i = 0; i < rows; ++i) {
        matrix[i] = new int[cols];
    }

    // Assign values to elements
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            matrix[i][j] = i * cols + j; // Sample assignment
        }
    }

    // Accessing elements
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            std::cout << matrix[i][j] << " ";
        }
        std::cout << std::endl;
    }

    // Deallocate memory (important to avoid memory leaks)
    for (int i = 0; i < rows; ++i) {
        delete[] matrix[i];
    }
    delete[] matrix;

    return 0;
}

				
			
				
					// output //
0 1 2 3 
4 5 6 7 
8 9 10 11 

				
			

Explanation:

  1. rows and cols define the dimensions of the matrix.
  2. matrix is a pointer to a pointer that will eventually point to an array of integers (each row).
  3. We allocate memory for rows number of integer pointers using new int*[rows]. This creates an array of pointers (matrix) where each element will point to a row of the matrix.
  4. We iterate through each row (matrix[i]) and allocate memory for cols number of integers using new int[cols]. This creates individual rows of the matrix.
  5. The nested loop iterates through each element and assigns a sample value (i * cols + j).
  6. The second loop demonstrates accessing elements using double dereference: matrix[i][j] first dereferences matrix[i] to get the pointer to the row, then dereferences that pointer to access the element at that index.
  7. Finally, we deallocate memory in the reverse order of allocation to avoid memory leaks. We delete each row (matrix[i]) and then the array of pointers (matrix) itself.

Function Pointers and Multidimensional Arrays

You can use pointers to pointers to pass multidimensional arrays to functions as arguments. This allows for more flexibility in function design.

				
					#include <iostream>

void printMatrix(int** matrix, int rows, int cols) {
  for (int i = 0; i < rows; ++i) {
    for (int j = 0; j < cols; ++j) {
      std::cout << matrix[i][j] << " ";
    }
    std::cout << std::endl;
  }
}

int main() {
  int rows = 2, cols = 3;
  int** matrix = new int*[rows];
  
  // Allocate memory for each row (array of integers)
  for (int i = 0; i < rows; ++i) {
    matrix[i] = new int[cols];
  }

  // Assign values to elements
  for (int i = 0; i < rows; ++i) {
    for (int j = 0; j < cols; ++j) {
      matrix[i][j] = i * cols + j; // Sample assignment
    }
  }

  printMatrix(matrix, rows, cols);

  // Deallocate memory
  for (int i = 0; i < rows; ++i) {
    delete[] matrix[i];
  }
  delete[] matrix;

  return 0;
}

				
			
				
					// output //
0 1 2 
3 4 5 


				
			

Explanation:

  1. printMatrix takes a pointer to a pointer (matrix), the number of rows (rows), and the number of columns (cols) as arguments.
  2. In main, we create a two-dimensional array using pointers to pointers and populate it (not shown for brevity).
  3. printMatrix is called with the matrix, rows, and cols as arguments, allowing the function to access and print the elements of the multidimensional array.

Sparse Matrices (Optional)

Sparse matrices are matrices that have a large number of elements which are zero. In contrast to dense matrices, which store all elements regardless of their value, sparse matrices only store non-zero elements and their indices. This storage approach is particularly useful when dealing with matrices in which a significant portion of the elements are zero, as it can save memory and improve computational efficiency.

There are several representations for sparse matrices, each with its own advantages and use cases. Three common representations are:

  1. Coordinate list (COO): In this representation, each non-zero element is stored along with its row and column indices. This representation is simple and flexible but can be inefficient for large sparse matrices.

  2. Compressed sparse row (CSR): CSR representation stores the non-zero elements in three arrays: one for the non-zero values, one for the column indices of each value, and one for the row pointers indicating where each row starts in the other two arrays. CSR is efficient for arithmetic operations like matrix-vector multiplication.

  3. Compressed sparse column (CSC): CSC representation is similar to CSR, but it stores the column indices instead of row indices. This representation is also efficient for arithmetic operations, especially column-wise operations.

Let’s look at a code example using the COO representation to create and print a sparse matrix:

				
					#include <iostream>
#include <vector>

struct Element {
    int row;
    int col;
    int value;
};

class SparseMatrix {
private:
    int rows;
    int cols;
    std::vector<Element> elements;

public:
    SparseMatrix(int rows, int cols) : rows(rows), cols(cols) {}

    void addElement(int row, int col, int value) {
        if (row >= 0 && row < rows && col >= 0 && col < cols) {
            Element e = {row, col, value};
            elements.push_back(e);
        } else {
            std::cerr << "Error: Index out of bounds." << std::endl;
        }
    }

    void print() {
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                int value = 0;
                for (const Element& e : elements) {
                    if (e.row == i && e.col == j) {
                        value = e.value;
                        break;
                    }
                }
                std::cout << value << " ";
            }
            std::cout << std::endl;
        }
    }
};

int main() {
    SparseMatrix sparse(3, 3);
    sparse.addElement(0, 0, 1);
    sparse.addElement(0, 2, 2);
    sparse.addElement(1, 1, 3);
    sparse.addElement(2, 2, 4);

    sparse.print();

    return 0;
}

				
			
				
					// outpu //
1 0 2 
0 3 0 
0 0 4 

				
			

In this example, we define a SparseMatrix class with a vector of Element structures to store the non-zero elements. The addElement method adds a new non-zero element to the matrix, and the print method prints the matrix.

Here, we’ve created a sparse matrix with dimensions 3×3 and added four non-zero elements at specific positions. The output shows the resulting sparse matrix, with zeros in the positions not specified. This demonstrates how sparse matrices can be represented efficiently by only storing non-zero elements.

Cautions and Best Practices

Dangling Pointers

Be mindful of dangling pointers when using pointers to pointers. A dangling pointer occurs when a pointer points to memory that has been deallocated. This can lead to undefined behavior.

Memory Management

Pointers to pointers introduce additional complexity in memory management. Ensure proper memory allocation and deallocation to avoid memory leaks.

Readability and Maintainability

Excessive use of pointers to pointers can make code harder to read and maintain. Use them judiciously when simpler approaches won’t suffice.

Remember

  1. Master single pointers before diving into pointers to pointers.
  2. Practice with small code examples to solidify your understanding.

Pointers to pointers offer a powerful tool for memory manipulation and advanced data structures in C++. By understanding the concepts of indirection, applications, and potential pitfalls, you can leverage them effectively in your C++ programs.Happy coding !❤️

Table of Contents