Arrays and Pointers
Let's look at the following line:
int var = 42;
In this case, a block of memory is reserved by the compiler to store an int value. An address is assigned to this block. To show the address, we must use the & operator:
printf("Address of var = %d\n", &var);
// It prints "Address of var = 1864086260"
The address is a random number that can change at each execution.
To obtain the value stored at a given address (for example, at address &var), we must use the * operator:
printf("Value of var = %d", *(&var));
// It prints "Value of var = 42"
The address of a variable can be stored in another variable known as a pointer variable. The syntax for storing the address of a variable in a pointer is:
type *namePtr = &nameVar;
For example:
int var = 42;
int *ptr = &var;
or
int var = 42;
int *ptr;
ptr = &var;
The data type tells the compiler what type of data the variable will contain whose address we are going to store. In our last example, ptr is a pointer to an int.
[!WARNING] This doesn't mean that
ptrwill store anintvalue. A pointer to an integer (likeptr) can only store the address of variables of typeint.
[!WARNING] Note that the asterisk
*is used both for declaring the pointer and for dereferencing it:int var = 42; int *ptr; // The * is used to declare a pointer ptr = &var; printf("Var = %d", *ptr); // The * is used to dereference the pointer (i.e., to show the value stored at address p = &var)
Wild Pointers
Those pointers that do not point to any address are called wild:
char *alphabetAddress; //Wild pointer
char alphabet = "a";
alphabetAddress = &alphabet; //No more wild
[!TIP] To ensure that we don’t have a wild pointer, we can initialize a pointer with a
NULLvalue, making it a null pointer:char *alphabetAddress = NULL;
Pointer Arithmetics
One can only add or subtract integers to pointers:
int array[4] = {5,10,15,20};
int *ptr = &array[0];
ptr += 3 // This is valid
ptr *= 3 // This is NOT valid
[!WARNING] When you add (or subtract) an integer (say
n) to a pointer, you’re NOT actually adding (or subtracting)nbytes to the pointer’s value. Instead, you are adding (or subtracting)ntimes the size in bytes of the data type of the variable being pointed to.int a = 5; int *ptr = &a; // Assume the address is 1000 int newAddress = ptr + 3; // It is equal to: ptr + 3*sizeof(int) = 1000 + 3*4 = 1012
Let's look at the follwing code:
#include <stdio.h>
int main(){
int arr0D = 42;
int *p0 = &arr0D;
printf("-------------------------------\n");
printf("0D array (variable) and pointer\n");
printf("-------------------------------\n");
printf("Number of element of arr0D = %d \n", sizeof(arr0D)/sizeof(int));
printf("Sizeof(arr0D) = %d Bytes\n\n", sizeof(arr0D));
printf("arr0D = %d ; &arr0D = %d\n", arr0D , &arr0D);
printf("*p0 = %d ; p0 = %d\n\n", *p0 , p0 );
printf("*(p0+1) = %d ; p0+1 = %d\n\n", *(p0+1) , p0+1 );
whose output is:
-------------------------------
0D array (variable) and pointer
-------------------------------
Number of element of arr0D = 1
Sizeof(arr0D) = 4 Bytes
arr0D = 42 ; &arr0D = 1830580912
*p0 = 42 ; p0 = 1830580912
*(p0+1) = 1 ; p0+1 = 1830580916
We immediately see that, since p0 = &arr0D, it means that p0 is storing the address of arr0D. To obtain the corresponding value at address &arr0D, we must dereference p0 (i.e., *p0). If we then add 1 to p0, we obtain the memory address stored at the same address of &arr0D (i.e., 1830580912) PLUS 4 Bytes (i.e., 1830580916). If we dereference *(p0+1), we obtain some trash value.
1D arrays and pointers
We now consider the following code:
#include <stdio.h>
int main(){
int arr1D[5] = {1,2,3,4,5};
int *p1 = arr1D;
printf("-------------------------------\n");
printf("1D array and pointer\n");
printf("-------------------------------\n");
printf("Number of element of arr1D = %d \n", sizeof(arr1D)/sizeof(int));
printf("Sizeof(arr1D) = %d Bytes\n\n", sizeof(arr1D));
printf("arr1D = %d\n", arr1D); //Points to element 0
printf("&arr1D[0] = %d\n", &arr1D[0]); //Points to element 0
printf("&arr1D = %d\n", &arr1D); //Points to the whole array
printf("p1 = %d\n\n", p1); //Points to the element 0
printf("&arr1D[1] = %d\n\n", &arr1D[1]); //Points to element 1
printf("Summing +1:\n");
printf("arr1D +1 = %d\n", arr1D+1);
printf("&arr1D[0]+1 = %d\n", &arr1D[0]+1);
printf("&arr1D +1 = %d\n\n", &arr1D+1);
printf("p1 +1 = %d\n\n", p1+1);
whose output is
-------------------------------
1D array and pointer
-------------------------------
Number of element of arr1D = 5
Sizeof(arr1D) = 20 Bytes
arr1D = 1832891120
&arr1D[0] = 1832891120
&arr1D = 1832891120
p1 = 1832891120
&arr1D[1] = 1832891124
Summing +1:
arr1D +1 = 1832891124
&arr1D[0]+1 = 1832891124
&arr1D +1 = 1832891140
p1 +1 = 1832891124
The array arr1D has 5 elements: since each of them is an integer (4 Bytes), the total size of arr1D is Bytes = 20 Bytes.
Notice that arr1D and &arr1D[0] both point to the 0th element of the array arr1D. Thus, the name of an array is itself a pointer to the 0th element of the array. Here, both point to the first element, which has a size of 4 Bytes. When you add 1 to them, they now point to the element with index 1 of the array (i.e., &arr1D[1]), this resulting in an address increment of 4 Bytes.
On the other hand, &prime is a pointer to an array of 5 integers. It holds the base address of the array arr1D[5], which is the same as the address of the first element. Therefore, increasing by 1 results in an address increment of 5 x 4 = 20 Bytes.
[!NOTE] The name of an array is itself a pointer to the 0th element of the array, and if we increase it by one, we move to the address of the next element. On the other hand,
&arr1Dis a pointer to the whole array, and increasing it by one results in an address increment ofsizeof(arr1D).In short,
arrand&arr[0]point to the 0th element, while&arrpoints to the entire array.
We can access the elements of the array using indexed variables like this:
int arr = {5,10,15,20,25};
for (int i = 0; i < 5; i++){
printf("index = %d, address = %d, value = %d", i, &arr[i], arr[i]);
}
or we can do the same thing using pointers, which are generally faster than using indexing:
int arr = {5,10,15,20,25};
for (int i = 0; i < 5; i++){
printf("index = %d, address = %d, value = %d", i, arr+i, *(arr+i));
}
[!TIP] Accessing the elements of the array using pointers is faster. If you don't believe... try!
2D arrays and pointers
In this section, we consider 2D arrays (but the discussion can be easily extended to multidimensional arrays).
Suppose we want to allocate an array to represent the 3 components of the velocity for each of the particles. To allocate it dynamically, we must use pointers:
arr = (float **)malloc(N*sizeof(float *));
for (int i = 0; i < N; i++) {
arr[i] = (float *)malloc(3 * sizeof(float));
}
Here, arr is declared as a pointer to a pointer to a float. This type of pointer is used to represent a 2D array because, in C, a 2D array is essentially an array of arrays. Each pointer in the first dimension (arr[i]) will point to a one-dimensional array of floats (the columns).
To allocate the first dimension (rows), we have used:
arr = (float **)malloc(N * sizeof(float *));
This line allocates memory for N pointers to floats, where N represents the number of rows. The malloc function dynamically allocates memory, and sizeof(float *) ensures that the correct amount of memory is reserved for each pointer. Since arr is a pointer to a pointer, it needs memory for storing N pointers that will later point to the actual rows of floats.
[!NOTE] The
(float **)in front of themalloc()call is a type cast. Indeed,malloc()returns a pointer of typevoid *. Avoid *pointer is a generic pointer that can point to any type of data, but it doesn’t carry type information itself. Therefore, when you assign the result ofmalloc()to a variable of a specific pointer type (likefloat **in your case), you need to cast thevoid *pointer to the appropriate type so that the compiler knows how to interpret it. So, the(float **)cast is used to explicitly convert thevoid *returned bymalloc()into afloat **, which is necessary for the variablearr.
To allocate the second dimension (columns), we have used:
for (int i = 0; i < N; i++) {
arr[i] = (float *)malloc(3 * sizeof(float));
}
This loop allocates memory for each row. For each i from 0 to N-1, arr[i] is allocated memory to hold 3 float values. This creates a row with 3 columns for each row in the 2D array. The sizeof(float) ensures that the correct amount of memory is allocated for each float. The number 3 here specifies the number of columns in each row.
To summarise, we created a pointer (arr) that points to an array of pointers. Each of these pointers will, in turn, point to an array of floats. The first call to malloc allocates enough memory to hold N pointers, one for each row. This is why arr is of type float **. For each row, the second malloc allocates memory for 3 floats. These floats represent the columns in each row, creating the second dimension of the array.
[!WARNING] Memory Management: Since you’re dynamically allocating memory, it’s essential to free the allocated memory later to avoid memory leaks. This would be done using
free()for each row and finally for the array of pointers:for (int i = 0; i < N; i++) { free(arr[i]); // Free each row } free(arr); // Free the array of pointers
The follwing example shows also how to pass this 2D array to a function:
#include <stdio.h>
#include <stdlib.h>
void func(float **arr, int dim1, int dim2){
for(int i = 0; i<dim1; i++){
for(int j = 0; j<dim2; j++){
*(*(arr+i)+j) = i*dim2+j;
}
}
}
int main(){
int const N = 10;
float **arr;
arr = (float **)malloc(N*sizeof(float *));
for (int i = 0; i < N; i++) {
arr[i] = (float *)malloc(3 * sizeof(float));
}
func(arr, N, 3);
for (int i = 0; i < N; i++) {
for (int j = 0; j < 3; j++) {
printf("%d: %f\n",i, *(*(arr+i)+j));
}
}
for (int i = 0; i < N; i++) {
free(arr[i]); // Free each row
}
free(arr); // Free the array of pointers
return 0;
}
Note that in the function func we are using pointers arithmetics: the expression *(*(arr + i) + j) is used to access the elements of a 2D array:
arris a pointer to a pointer. Essentially,arris a pointer to the first element of an array of pointers, where each pointer points to the first element of a row in the 2D array.arr + iadvances the pointerarrbyipositions. Sincearris a pointer to pointers(float **arr),arr + imoves the pointeristeps ahead, pointing to the i-th row of the array.*(arr + i)dereferences the pointer to the i-th row, i.e., it accesses the pointer that points to the first element of the i-th row. In simpler terms,*(arr + i)gives you the address of the first element in the i-th row.*(arr + i) + j: from the address of the i-th row (which is itself a pointer), you movejpositions ahead to reach the j-th element of the i-th row.*(arr + i) + jgives you a pointer to the element at positionarr[i][j].- ((arr + i) + j): finally, the outer
*dereferences the pointer obtained from the previous step, giving you the actual value of the element at positionarr[i][j].
