Pointers to Pointers in C
In this article we will explore the concept of pointers to pointers in C language.
Pointers to pointers are powerful tools that allow us to manage complex data structures such as dynamic matrices, structures with dynamic members, and jagged arrays.
Through practical examples, we will see how to declare, initialize, and use pointers to pointers to manipulate data efficiently and flexibly. We will also discover how to allocate and free memory dynamically for these structures, ensuring optimal resource management.
What is a Pointer to Pointers?
In C language, we have seen that a pointer is able to store the memory address of any object in memory as long as its type is specified. For example, we can create a pointer to int, char, float, double, etc.
We can even create pointers to structures, unions, and enumerations. Taking this reasoning forward, we can create a pointer that points to another pointer. This type of pointer is called a pointer to pointers. We have already studied them indirectly when we saw string arrays (jagged arrays) and command line arguments.
A pointer to pointers is a variable that contains the address of another pointer. In C, the syntax for declaring a pointer to pointers is similar to that for declaring a single pointer, but with an additional asterisk. For example, int **ptr declares a variable ptr that is a pointer to a pointer to an integer. To assign a value to a pointer to pointers, it is necessary to first have a pointer to an integer and then assign the address of this pointer to the pointer to pointers. Here is an example:
int a = 10;
int *p = &a;
int **pp = &p;
In this example, a is an integer variable, p is a pointer to a, and pp is a pointer to p. Using pp, it is possible to access the value of a indirectly.
Graphically, we can see the above example in this way:
Through the pointer pp we can both access the pointer p and access the value of a. This depends on the number of times we use the dereference operator *.
/* Without dereference operator
* we access the address of p */
printf("Address of p: %p\n", pp);
/* Applying one dereference operator
* we access the value of p
* that is the address of a */
printf("Address of a: %p\n", *pp);
/* Applying two dereference operators
* we access the value of a */
printf("Value of a: %d\n", **pp);
As can be noted in the above example, in the first case we access the address of p, that is the value that is actually stored in pp.
Applying only once the dereference operator *, we access the value of p, that is the address of a.
Finally, applying twice the dereference operator *, we access the value of a.
Recapping:
Pointer to Pointers
A Pointer to Pointers is a pointer variable that contains the address of another pointer.
To declare a pointer to pointers, the following syntax is used:
type **pointer_name;
To initialize a pointer to pointers, it is necessary to have a pointer to a data type and assign the address of this pointer to the pointer to pointers:
type *pointer = &variable;
type **pointer_to_pointers = &pointer;
To access the final value (that is the value of the variable pointed to by the pointer to which the pointer to pointers points), it is necessary to apply the dereference operator * as many times as there are levels of indirection:
**pointer_to_pointers;
This mechanism can be extended to multiple levels of indirection.
Example: Dynamic Matrices
One of the most common uses of pointers to pointers is the creation of dynamic matrices. A dynamic matrix is a matrix whose number of rows and columns is defined at runtime. To create a dynamic matrix, we can use a pointer to pointers.
Let's take an example: suppose we want to create a matrix of 3 rows and 4 columns composed of floating-point numbers. We can declare the matrix in this way:
int rows = 3;
int cols = 4;
double **matrix;
To allocate memory for the matrix, we must first allocate memory for the rows and then for the columns. Here is an example of how we can do it:
/* We first allocate memory for the rows */
matrix = (double **) malloc(rows * sizeof(double *));
In the above code we must note that we used the sizeof operator to calculate the size of a pointer to double and not the size of a double. This is because matrix is a pointer to pointers to double.
The result is represented in the figure:
Now we can allocate memory for the columns:
for (int i = 0; i < rows; i++) {
matrix[i] = (double *) malloc(cols * sizeof(double));
}
Having done this, the result is that reported in the following figure:
Now we can access the elements of the matrix as follows:
matrix[0][0] = 1.0;
matrix[0][1] = 2.0;
matrix[0][2] = 3.0;
/* ... */
We must pay attention when we want to free the memory allocated for the matrix. We must first free the memory for each individual row and then the memory for the matrix. Here is an example:
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
The use of pointers to pointers to create dynamic matrices is one possible way to manage them. The other way is, as we have seen, that of flattening the matrix and using a dynamic one-dimensional array. This second method is more efficient in terms of data access, but is less intuitive.
Example: Structures with Dynamic Members Allocated Dynamically
Another common use of pointers to pointers is to create structures with dynamic members allocated dynamically. Suppose we want to create a Person structure that contains a name and a surname, both allocated dynamically. We can define the structure in this way:
typedef struct {
char *name;
char *surname;
} Person;
To allocate memory for a variable of type Person, we must allocate memory for the name and surname members. Here is an example of how we can do it:
Person *person = (Person *) malloc(sizeof(Person));
But this is not enough. We must allocate memory for the name and surname members:
person->name = (char *) malloc(100 * sizeof(char));
person->surname = (char *) malloc(100 * sizeof(char));
Now we can assign values to the name and surname members:
strcpy(person->name, "Mario");
strcpy(person->surname, "Rossi");
Finally, we can free the memory allocated for the person variable:
free(person->name);
free(person->surname);
free(person);
Technically, person is not a pointer to pointers but rather a pointer to a struct. However, person->name and person->surname are members of person that are in turn pointers. Therefore we have exploited double indirection implicitly through the person pointer.
Example: Dynamic Jagged Arrays
We have already encountered Jagged Arrays or Irregular Matrices when we talked about string arrays. A jagged array is an array of arrays of different sizes. They are very similar to the matrices seen above but with the difference that each row can be an array of different sizes.
Let's see an example. Suppose we want to represent a set of student classes. Each class is composed of a set of different students, especially the classes can also differ in the number of students present. We can represent this set of classes with a jagged array.
Let's start by defining the data structure that represents a student, with their name and age:
typedef struct {
char *name;
int age;
} Student;
Now we can define the set of classes as an array of arrays of students:
int num_classes = 3;
int num_students[] = {2, 3, 4};
Student **classes = (Student **) malloc(num_classes * sizeof(Student *));
A class in this case is an array of students. Therefore we must allocate memory for each class:
for (int i = 0; i < num_classes; i++) {
classes[i] = (Student *) malloc(num_students[i] * sizeof(Student));
}
Now we can assign values to the name and age members of each student:
strcpy(classes[0][0].name, "Mario");
classes[0][0].age = 20;
strcpy(classes[0][1].name, "Luigi");
classes[0][1].age = 21;
/* ... */
Finally, we can free the memory allocated for the classes:
for (int i = 0; i < num_classes; i++) {
free(classes[i]);
}
free(classes);
In this example, we used a jagged array to represent a set of student classes. Each class is an array of students, and the number of students in each class can vary.
In Summary
In this article we have added another piece to our knowledge base about pointers in C language.
We have seen that:
- A pointer to pointers is a variable that contains the address of another pointer.
- To declare a pointer to pointers, the syntax
type **pointer_nameis used. - To initialize a pointer to pointers, it is necessary to have a pointer to a data type and assign the address of this pointer to the pointer to pointers.
- To access the final value (that is the value of the variable pointed to by the pointer to which the pointer to pointers points), it is necessary to apply the dereference operator
*as many times as there are levels of indirection.
We have also seen some practical examples of how to use pointers to pointers to create dynamic matrices, structures with dynamic members allocated dynamically, and jagged arrays.
Now that we know how to create and use dynamic jagged arrays, in the next lesson we will address dynamic string arrays.