In the previous post we learnt what pointers are, their properties, and how to use them. However, most likely this didn’t help understand why pointers are so useful or why we even need them at all. Today we’re going to take care of that.
Pointers are most valuable when combined with other functionalities of the C language. Let’s start by taking a look at how they work with arrays.
Working with arrays using pointers
In the previous chapter we saw that if we have a pointer to int and we increment it, now the pointer contains the address of the next int, and the same happened with pointers to char. Let’s see a short example to refresh our memory:
The program above declares two variables (an int and a char) and a pointer to each of them. Integer variables take up two bytes in memory, so incrementing a pointer to int makes it point to an address two bytes higher. Similarly, character variables only take up one byte and incrementing a pointer to char makes it point to an address one byte higher:
However, note that in this particular example it isn’t very useful to increment these pointers. Showing a diagram of what’s in the computer’s memory will help understand why:
In this case the variables and pointers are arranged in memory in the order they were defined in the program. Remember that a pointer is also a variable and it also takes up two bytes in memory. The reason why pointer arithmetic isn’t useful in this scenario is that incrementing the pointer doesn’t make it point to the correct type of data. See this:
On the left diagram we see what happens when we increment pi:
- pi + 1 doesn’t point to an integer, but to a pointer to integer
- pi + 2 points to a char instead of an integer
- pi + 3 points to the high byte of a pointer to integer
- pi + 4 points to an unused memory address
The same goes for pc on the right:
- pc + 1 points to the low byte of a pointer to integer
- pc + 2 points to the high byte of a pointer to integer
- pc + 3 points to an unused memory address
Pointers didn’t help much in this case.
However, consider what happens when we’re working with an array. All the elements in an array are perfectly aligned in memory one after the other. This is when things get interesting: if we have an array of integers called a, and a pointer p to the element a[i], then p+1 will certainly point to the next element in the array, a[i+1].
Let’s see an example. The program below defines an array a that contains three integers, and p is a pointer to int. We can use p to access all the elements in the array:
In this case the memory is arranged as in the diagram below. It’s easy to see that incrementing the pointer will allow us to easily access each of the elements in the array:
Compile and run the program to confirm that it works as expected:
We can optimize this program a bit. In the example we used the i variable to iterate inside the for loop, but we can simplify and use the pointer as the loop variable instead:
This version will do exactly the same as the previous one (compile and run it to confirm), but using one less variable. At the beginning it’s normal to feel that pointers make the program a bit more difficult to follow, but when we get used to working with pointers they often feel more natural and are easier to use.
We can still optimize the previous program a bit more, because…
Array names are actually pointers
In the program POINT3.C we referred to the address of the first element of the array a like this:
However, we could also have written the same thing like this:
That’s not a typo. That’s just the name of the array a. This is because in C, the name of an array is always a pointer to its first element. Let’s confirm this with another example. The program below should print the same value for both &a and a:
So, if a is a pointer to a then a+1 will be a pointer to a, or, in other words, is equivalent to &a. Based on this it follows that in general, a+i is exactly the same thing as &a[i] regardless of the type of the array a.
Maybe you can see already how we can use this to make C functions even more powerful…
Using pointers in function declarations
We learnt some time ago that we can’t use an array as a function parameter. However, we can achieve the same thing by passing a pointer to an array. Similarly, we can use pointers to implement functions that return more than one parameter and also use them to modify local variables that belong to other functions.
Let’s see how these techniques work.
Passing pointers as function arguments
Using pointers as function arguments works just like passing any other value. We just need to declare the type of the argument so the compiler knows we’re passing a pointer and not something else. For example, the program below defines a function called cleari() that takes a pointer to an integer and clears the value pointed to:
This program follows a very simple flow:
- When main() starts the variable i has some memory address already assigned, but its content is still undefined (random) because the program hasn’t initialized it. At this point the pointer p doesn’t even exist yet.
- main() calls cleari() and passes it the address of variable i. Inside cleari() the pointer p is created and assigned the address of i.
- cleari() sets the integer value pointed to by p to 0.
- cleari() ends and now main()‘s i variable is set to 0.
Notice what happened here: inside the cleari() function main()‘s local variable i was reset to 0. We already know that a function’s local variables can’t be accessed by other functions. However, using pointers allows us to bypass this restriction and perform operations with memory addresses that belong to other parts of the program.
Using pointers to implement functions that return more than one value
We’ve just seen that functions accept pointers as parameters and that in this way we can modify variables that belong to other functions. We can use this technique to our advantage in order to implement functions that return more than one value.
As an example, consider the addsub() function below. It takes two integer parameters and returns both their addition and their substraction:
This addsub() function takes four arguments. The first two are the two integer values that we want to add and substract. The other two (s and d) are pointers to integer that the function will use to store the results. Notice that in this case we’re declaring the return type of the function as VOID to indicate that it doesn’t return any value. Instead, the results are stored directly in the addresses pointed by s and d.
To use this function we just need to pass it the two integer numbers to add/substract and the addresses of two integer variables to store the results. For example, to add and substract the numbers 100 and 700 and store the results in the variables sum and dif we would call it like this:
addsub(1000, 700, &sum, &dif);
After this function call the variable sum will contain 1700 and dif will contain 300.
Implementing functions that work with arrays
We can’t pass an array as a parameter to a function, but we can pass a pointer to any element in it and use pointer arithmetic to access each of the elements in turn. Consider the nprint() function below:
nprint() takes two arguments: a pointer to an integer (p) and an integer number (n). It then prints n integer numbers on the screen starting from the one pointed by p. For example, if we have an integer array a that contains 5 elements we can print all of them by just calling nprint(a, 5) as in the program below:
In this simple way we can pass a whole array as an argument and access its elements from inside a function. Compile and run the program to confirm that it works as expected:
Actually, we can use the same function to print part of the array by passing it the address of some element in the middle and the number of elements to print from there. For example, to print the three elements in the middle of the array:
Note that in the POINT6.C program we passed nprint() the name of the array a. We could also have called it passing the address of the first element: nprint(&a, 5). In practice they’re both the same, though each notation has its nuance:
- a refers to the start address of the array as a whole.
- &a refers to the address of an element in the array, in this case the first one.
The compiler doesn’t care which notation you use. Feel free to write it in whichever way makes you feel more comfortable.
In this chapter we’ve learnt how to use pointer arithmetic to access each of the elements of an array and we’be seen that array names are actually pointers to the first element in the array. We’ve also seen how to pass pointers as arguments to functions and how to use this to expand the capabilities of C functions.
In the next post
Now that we’ve learnt about pointers and their relationship with arrays, next we’ll take a deeper look at strings in C.
This series of articles is supported by your donations. If you’re willing and able to donate, please visit the link below to register a small pledge. Every little amount helps.