Road to C Programmer #5 - Pointers

Last Edited: 7/29/2024

The blog post is about one of the most important concepts in C, pointers.

C Pointers

Pointers

Pointers are arguably the most important and hardest concept to learn in C. To understand pointers, you need to understand how variables are stored in memory. When you declare and initialize a variable like int x = 5;, what's actually happening behind the scenes is that it stores the value 5 at an address represented by a hexadecimal number and refers to that hexadecimal number as x. Similarly, when you store an array like int y[3] = {1, 2, 3};, it stores three values next to the addresses next to each other and refers to the address of the first element of an array as y.

How they are stored in RAM
Variable NameAddressValue
x0x16dc6f1105
y0x16dc6f1181,
2,
3

To print the address of the variable stored, we can use %p (where p is short for pointer) as the format specifier and &x for displaying the address of x. The pointer is just a variable storing the address of another variable, which can be defined as int *z = &x. int * refers to the int pointer, meaning that it stores the address of an int variable. After declaring and initializing the pointer, the memory looks like this:

How they are stored in RAM
Variable NameAddressValue
z0x16dc6f1080x16dc6f110
x0x16dc6f1105
y0x16dc6f1181,
2,
3

When you want to retrieve a value stored in an address from a pointer, meaning if you want to retrieve the value of x from the pointer z, you can use *z, called the dereference operator.

Why Pointers?

Well, you might be wondering why pointers are useful at all. Let's look at an example where using pointers is instrumental. Suppose you want to have a function that can swap the values of the variables outside of the main function. You might come up with a function like this:

void swap (int x, int y) {
    int temp = x;
    x = y;
    y = temp;
}
 
int main () {
    int a = 5;
    int b = 10;
 
    swap(a, b);
 
    printf("a: %d, b: %d", a, b);
 
    return 0;
}

However, if you run the above, you get a: 5, b: 10. This is because if you pass variables to a function in C, the function takes the values of the variables, not the address. Hence, calling swap(a, b) is the same thing as calling swap(5, 10). This is called call by value. Instead of doing this, we can use pointers:

void swap (int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}
 
int main () {
    int a = 5;
    int b = 10;
 
    swap(&a, &b);
 
    printf("a: %d, b: %d", a, b);
 
    return 0;
}

Now, instead of passing variables, we are passing pointers set to the addresses of a and b. Then, what happens is that swap is appropriately using the addresses and their corresponding values with dereference operator to swap the values, successfully printing a: 10, b: 5. This is called call by pointer (or call by reference), and this is one of the main reasons why pointers are so important in C.

Arrays and Pointers

We understood how pointers work for int from the above, and we can deduce that it works the same for char, float, and double. Then, how about arrays? Let's look at an example to understand how pointers work on arrays.

void addOne (int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] += 1;
    }
}
 
int main () {
    int xs[3] = {1,2,3};
    int size = sizeof(xs)/sizeof(xs[0]);
 
    addOne(xs, size);
    printf("%d %d %d", xs[0], xs[1], xs[2]);
    
    return 0;
}

We've seen that if we pass a variable to a function, it does not work as expected because it calls by value. If that applied here as well, we would expect the above to not work and just print out 1 2 3. However, if you run the above, it prints out 2 3 4. Why is that? It is because when we pass an array to a function, the array will decay to a pointer and start to behave like a pointer. A pointer can also behave like an array as follows.

int xs[3] = {1,2,3};
int *p = xs;
 
printf("%d", p[2]); // 2

This is because arr[i] notation is just for accessing the value of an address, i steps away from the arr, which corresponds to the address of the first element of the array. As arrays store their elements next to each other, arr[i] can retrieve the value stored in the ith element of an array. This means even the below is legal:

int main () {
    int x = 3;
    int *px = &x;
    printf("%d", px[1]);
    return 0;
}

Even though the above pointer is pointing to x, which stores an integer, not an array, we can use px[1] to access the value that happens to be stored next to x. You can achieve the same thing as [] with pointer arithmetic as well.

int xs[3] = {1,2,3};
int *p = xs;
 
printf("%d", *(p + 1)); // 2
printf("%d", *(xs + 1)); // 2
 
p++;
xs+=2;
 
printf("%d", *(p)); // 2
printf("%d", *(xs)); // 3

You can add 1 to a pointer to move to the next address and use the * dereference operator to access that value. As an array decays to a pointer, arrays work the same. If they behave so similarly like that, then are they the same thing? Well, there was a reason for me to say the array decays to a pointer instead of saying they are the same.

int xs[3] = {1,2,3};
int ys[3] = {4,5,6};
int *p = xs;
 
// This is possible
p = ys;
 
// This is NOT possible
xs = ys;

While a pointer can be set to a new address anytime, an array cannot be reassigned like that. There is another important difference between array and &array pointers.

int matrix[3][5] = {
    {1,2,3,4,5},
    {6,7,8,9,10},
    {11,12,13,14,15}
};
 
printf("matrix[1]+1: %d\n", *(matrix[1]+1));
printf("&matrix[1]+1: %d\n", *(&matrix[1]+1));
// matrix[1]+1: 7
// &matrix[1]+1: --The address of 12--

The result of pointer arithmetic with an aray matrix[1]+1 is different from with an &array pointer &matrix[1]+1. After deferencing the &array pointer, we will just get back an address storing 12. The reason behind it is that the &array pointer is actually pointing to the whole array instead of the first element of the array. Because of this, when we do &matrix[1]+1, it moves to the next row of the array, instead of moving to the next element.

In addition to that, the dereferencing did not work because the &array pointer stores the pointer to the first element. Hence, when we dereference it again like *(*(&matrix[1]+1)), we will get 12. Another way of getting 12 is to cast the type of &matrix[1]+1 as an int pointer, like int *p = (int *) (&matrix[1]+1), so that we can dereference *p.

This is an extremely important distinction to make when dealing with arrays and pointers.

Exercises

From this article, there will be an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.

Resources