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.
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
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)
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.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)
num
stores the value 42.ptr
is a regular pointer that points to num
.ptr_ptr
is a pointer to a pointer that points to ptr
.std::cout
directly accesses num
and prints its value (42).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).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).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.
C++ doesn’t support built-in two-dimensional arrays. However, you can simulate them using pointers to pointers. Here’s how:
#include
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
rows
and cols
define the dimensions of the matrix.matrix
is a pointer to a pointer that will eventually point to an array of integers (each row).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.matrix[i]
) and allocate memory for cols
number of integers using new int[cols]
. This creates individual rows of the matrix.i * cols + j
).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.matrix[i]
) and then the array of pointers (matrix
) itself.You can use pointers to pointers to pass multidimensional arrays to functions as arguments. This allows for more flexibility in function design.
#include
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
printMatrix
takes a pointer to a pointer (matrix
), the number of rows (rows
), and the number of columns (cols
) as arguments.main
, we create a two-dimensional array using pointers to pointers and populate it (not shown for brevity).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 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:
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.
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.
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
#include
struct Element {
int row;
int col;
int value;
};
class SparseMatrix {
private:
int rows;
int cols;
std::vector 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.
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.
Pointers to pointers introduce additional complexity in memory management. Ensure proper memory allocation and deallocation to avoid memory leaks.
Excessive use of pointers to pointers can make code harder to read and maintain. Use them judiciously when simpler approaches won’t suffice.
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 !❤️