Pointers and more pointers
Many blog posts and tutorials would have it that the C programming language just about begins and ends with pointers. As we have seen, you can get a lot done with C while (just about) ignoring this topic but some of the flexibility and raw power of the language is lost if pointers are not mastered (or at least tamed).
A variable pointer
We have bumped into them a little earlier but we should start from the beginning.
Say we have an int variable x with a value of 5;
int x = 5;
we can declare a pointer to x as follows:
int* xPointer = &x;
(The * can float so int *xPointer = &x; means the same.)
The & operator returns the memory address of a variable. So, we have stored the address of x in the pointer xPointer. You will have noted that the pointer was defined as relating to a “type” of int. Identifying the pointer type ensures that the compiler knows how many bytes of memory are associated with the address the pointer is holding. The xPointer pointer can be used to point to any int variable address but for the moment at least points to the address of the int variable x. An expression can now access the value of x via the pointer using the * operator.
Serial.println(*xPointer);
Here the * dereferences the pointer (a clumsy term) and passes the value stored at the address to Serial.println(). I hope this demonstrates that there is nothing weird or magical about pointers. They are simply another way of accessing a value stored at a given memory location.
It has already been mentioned in the section on functions that when an array is passed to a function as an argument it is not passed by value (where a copy of a variable is made and passed to the function) but by reference. When an array is an argument then a pointer to the address of the first element of the array is passed to the function.
When we write code to access an indexed element of an array what actually happens under the hood is that a calculation is used to get the address of the required element. The pointer is incremented by a multiple of the number of bytes used to store the array data type. The value stored at that calculated address can be retrieved and used in an expression.
The next code sample illustrates incrementing pointers and how arrays are passed to a function.
void setup() { Serial.begin(115200); int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; int* po = &array[0]; for (int i = 0; i < 9; i++) { Serial.print("Pointer value: "); Serial.print((int)po); Serial.print(" array value: "); Serial.println(*po); po++; } foo(array); } void foo(int array[]) { Serial.print("Pointer to array: "); Serial.println((int)array); Serial.print("value at array[0]: "); Serial.println(*array); }
If you type in this code and run it, you should see the Serial Monitor display values like:
Pointer value: 2282 array value: 1
Pointer value: 2284 array value: 2
Pointer value: 2286 array value: 3
Pointer value: 2288 array value: 4
Pointer value: 2290 array value: 5
Pointer value: 2292 array value: 6
Pointer value: 2294 array value: 7
Pointer value: 2296 array value: 8
Pointer value: 2298 array value: 9
Pointer to array: 2282
value at array[0]: 1
The first nine lines of output show the address held in the pointer po and the value stored at that location. The last two lines confirm that the argument “int array[]” passed to the function foo() is in fact a pointer to the first element of array[].
The statement Serial.print((int)po); might need some additional explanation. The (int) in front of the pointer variable name po “casts” the pointer value to an int type so that it can be correctly displayed by the Serial.print() function. If this cast was omitted, then the compiler would have found the expression ambiguous as it would not have been clear that we wanted the address itself and not the content of that address location.
I hope it is clear that the above sample output from the code example was run on an Arduino where two bytes are used to store an int variable type.
Have a play with the code until you are happy that once a pointer has been declared and then incremented (or decremented) it behaves like a pointer and not like an integer variable. Also, you should be confident that a pointer is initialised with an address using the & operator and that the value stored at the address held as a value for a given pointer can be accessed using the * dereference operator. You might later like to reflect on how the characteristics of pointers simplify the implementation of arrays in C.
Before going further, add the following line to the test program – as the last line in the setup() function:
Serial.println((int)foo);
Run the program again. I think you might see that it looks rather like function names are a type of pointer as well. Before we look at function pointers we should press on further with arrays as well as looking at some different pointer declarations. Both of these have the potential to lead to confusion and make pointers appear tricky when they should be straightforward.
Arrays of pointers
Pointers are variables and so you can have arrays of them just like any other data type. If you were to have an array of arrays (known as a multi-dimensional array) then what you actually end up with is an array of pointers to the individual arrays stored in memory. Multi-dimensional arrays are the next topic but before we get to them it is worth mentioning an alternate use for an array of pointers. If you had a set of large data items that would present problems if you wanted to order them in some way then you could order (or sort) an array of pointers to those data items instead. Your program could then access the items in the sorted order through the pointers. Later in the book there is a section on sorts that provide some nice working examples of this approach.
Multi-dimensional arrays
Single dimension arrays can be thought of as a list of values that share a single variable name – in fact that is just about how an array was defined in this book. As an example, you might have an array holding the ages of all of the occupants of a house. Perhaps something like:
int ages[] = {1, 16, 18, 58, 69};
If we wanted an array that could store the ages of every occupant of a street of houses we might start with the first three houses and something like:
int streetAges[][5] = {{1, 16, 18, 58, 69},{29, 26, 3, 1}, {45, 43, 21, 19}};
The statement above is effectively defining an array of int arrays. This example would be called a two-dimensional array. You will have noticed that this statement left it to the compiler to decide the size of the first array dimension but that the size of the arrays forming the second dimension has been set. All array lengths (also known as bounds) must be set except the first even when explicitly defining the array contents as shown.
The underlying structure of a two-dimensional array can be exposed nicely by passing such an array to a function as an argument. For this example, the code is using an array of strings but as each string is itself an array of type char we have a two-dimensional array. The following code will not compile but the error message is illuminating:
void setup() { Serial.begin(115200); char array[][9] = {"January", "February", "March", "April"}; inspect(array); } void inspect(char array[]){ for (int p = 0; p < 4; p++) { Serial.println(array[p]); } }
The error message includes:
cannot convert 'char (*)[9]' to 'char*' for argument '1' to 'void inspect(char*)'
which tells us what I should have passed as an argument is a set of pointers to one or more array with the length 9.
Correcting the declaration of the inspect() function to:
void inspect(char array[][9]){ …
allows the code to compile and run, sending the names of the months to the Serial Monitor.
I hope that you are just about convinced that C uses pointers to access indexed elements of an array – can we finally prove it here? If that is what happens, then in the following code, the pointer would have to increase the address pointed to a value that gives access to the start address of each of the individual char arrays. To test this hypothesis we can adjust the code as follows:
void setup() { Serial.begin(115200); char array[][9] = {"January", "February", "March", "April"}; inspect(array); } void inspect(char array[][9]){ for (int p = 0; p < 4; p++) { Serial.print("Pointer value: "); Serial.println((int)array); Serial.println(*array); array++; // increment the pointer } }
If you ran that code, you would have seen the Serial Monitor output confirm that incrementing the pointer (array) in the function inspect() stepped the address forward by the exact value needed to locate the string stored in the second dimension of the array.
Just for a moment, consider how pointers simplify the implementation of an even more complex three-dimensional array. Perhaps one like char months[12][2][9] that contains month names and their short form (starting “Jan” and “January” as an example).
char months[][2][9] = { {"Jan", "January"}, {"Feb", "February"}, {"Mar", "March"}, {"Apr", "April"}, {"May", "May"}, {"Jun", "June"}, {"Jul", "July"}, {"Aug", "August"}, {"Sep", "September"}, {"Oct", "October"}, {"Nov", "November"}, {"Dec", "December"} };
The actual char values are held in memory as a continuous block with the first 70 bytes looking something like:
'J' | 'a' | 'n' | 0 | 0 | 0 | 0 | 0 | 0 | 'J' | 'a' | 'n' | 'u' | 'a' | 'r' | 'y' | 0 | 0 | 'F' | 'e' |
'b' | 0 | 0 | 0 | 0 | 0 | 0 | 'F' | 'e' | 'b' | 'r' | 'u' | 'a' | 'r' | 'y' | 0 | 'M' | 'a' | 'r' | 0 |
0 | 0 | 0 | 0 | 0 | 'M' | 'a' | 'r' | 'c' | 'h' | 0 | 0 | 0 | 0 | 'A' | 'p' | 'r' | 0 | 0 | 0 |
0 | 0 | 0 | 'A' | 'p' | 'r' | 'i' | 'l' | 0 | 0 | 0 | 0 | etc | etc |
The pointer managing the primary array index needs to increment in steps of 18 chars while the pointer managing the secondary index increments in steps of 9 chars. Retrieving the string at array[3][1] simply requires calculating from the address of the first element of the array and then adding 3 x 18 plus 1 x 9. That should bring the pointer to the first character of “April”.
The following short program illustrates the difference between accessing those array values using the normal index notation as against the addresses of the data.
void setup() { Serial.begin(115200); char months[][2][9] = { {"Jan", "January"}, {"Feb", "February"}, {"Mar", "March"}, {"Apr", "April"}, {"May", "May"}, {"Jun", "June"}, {"Jul", "July"}, {"Aug", "August"}, {"Sep", "September"}, {"Oct", "October"}, {"Nov", "November"}, {"Dec", "December"} }; Serial.println(months[0][0]); // outputs “Jan” Serial.println(months[3][1]); // outputs “April” Serial.println(&months[0][0][0]); // outputs “Jan” Serial.println(&months[3][1][0]); // outputs “April” }
Which I hope confirms that (in the first two uses of Serial.println()) what was actually passed as the argument was the address of the zeroth element of the target string.
Pointers to pointers
Pointers can also point to pointers. Take a look at the following three lines of code:
int x = 7;
int* p1 = &x;
int **p2 = &p1;
You already know that p1 is a pointer to the variable x. Then p2 is defined as a pointer to the int pointer p1. Like regular variable types, pointers have an address in memory and a value. A pointer to a pointer can therefore be constructed in just the same way as a pointer to a more familiar variable type. A pointer to a pointer, indirectly points to the original variable.
Running the following code will unpack that indirection.
void setup() { Serial.begin(115200); int x = 7; int* p1 = &x; int **p2 = &p1; Serial.print("The address of x: "); Serial.println((int)p1); Serial.print("The value of x: "); Serial.println(*p1); Serial.print("The address of pointer p1: "); Serial.println((int)&p1); Serial.print("The value at P2: "); Serial.println((int)p2); Serial.print("The value P2 indirectly points to: "); Serial.println(**p2); Serial.print("The address of P2: "); Serial.println((int)&p2); }
The Serial Monitor should show something like the following:
The address of x: 2298
The value of x: 7
The address of pointer p1: 2296
The value at P2: 2296
The value P2 indirectly points to: 7
The address of P2: 2294
Here we can see that the pointers p1 and p2 have a memory address just like the variable x (which should not be a surprise) and that the pointer p2 can access p1 and the value stored for the int variable x. [In case you were wondering, int ***p3 = &p2; could continue with additional indirection.] The takeaway here is that if you see two asterisks in front of a variable being initialised then it is a pointer to a pointer and nothing more complex than that. Rest assured though, these are rare.
Declaring pointers (common slips)
int a = 7;
int b = 99;
int const *ptrX = &a;
int *const ptrY = &a;
The two pointers are not equivalent. You can write
ptrX = &b;
but you can’t write
*ptrX += 3;
as ptrX is defined as pointing to a read only location and respects that restriction. Interestingly, in this example it is pointing to a variable that is not itself read only. You can happily change the value of the int variable a but you can’t change the value at that location using the pointer ptrX.
You can however change the variable that ptrX points to. Feel free to give it a try in the Arduino IDE – as a reminder, it is a great idea to write some code to test anything written here that you feel not immediately clear.
The statement int *const ptrY = &a; above defined the pointer ptrY as a constant and cannot be changed so
ptrY = &b;
would rightly cause the compiler to complain.
Remembering subtle differences like these can be difficult – probably best to just remember to treat pointer declarations with the const modifier with caution and to look up (or test) the precise meaning if and when you run into this usage in a code example.
Now declaring two pointers:
int* pa, pb;
does not declare 2 pointers. pa is an int pointer and pb is an int variable. That is clearer if you remember that the line could have been written
int *pa, pb;
So logically
int *pa, *pb; // does declare two int pointers.
int (*pa), (((*pb))); is also valid with the brackets being ignored but they do lead us nicely to pointers to functions where brackets may be needed to be explicit about code intentions.
Pointers to Functions
We saw earlier that a function name looked suspiciously like a pointer. You may recall that we were able to “cast” a function name to an int and send that int value to the Serial Monitor where it was revealed as a memory address. Now we know more about pointers, it will now seem likely that C makes use of pointers to functions. This is great, because we programmers can use function pointers to enhance our code. Function pointers can be used to pass a function as an argument to another function or such pointers can be returned by a function. There are also many instances where an array of function pointers can be an effective way of implementing a range of potential responses to an event.
Perhaps we should start by differentiating between pointers to functions and functions that return a pointer. Here are two of the latter type:
void setup() { Serial.begin(115200); int* z = foo(4.5, 8); Serial.println(*z); char* nstr = func4(); Serial.println(nstr); } int *foo(float x, int y){ y *= x; return &y; } char *func4() { char* str = "Hello Pointer"; return str; }
Both foo() and func4() return a pointer. The function foo() returns an int pointer and func4() a char pointer. You might think after running this little program that func4() was a great way of returning an array (or string, as in this case) directly from a function. However, this approach is not recommended as the code is likely to break unpredictably if the reference within func4() is not released in time. While it is nice to sail a little dangerously when trying out language features it is better when coding for real to stick with the correct approach. If a function needs to return an array of values then create the array first and then pass it to the function as an argument (in effect pass a pointer to that array as an argument). The function can then fill in the array values.
When we declare a pointer to a function then we have to give that pointer a type just like any other pointer. We do this by describing the function return type and the types of any arguments. Thus:
int (*ptr)(float, int);
declares a pointer named ptr that can point to any function that returns an int and accepts two arguments (specifically a float and an int). Subsequently the pointer needs to be initialised by giving it the address of a suitable function. Note how the brackets “()” are arranged. The first pair encompass the named pointer declaration and the second two the data types to be passed as arguments. The initial “int”, as we have already said, declares the function return type.
void setup() { Serial.begin(115200); int (*ptr)(float, int); // declares the pointer type ptr = func1; // ptr is initialised int a = ptr(3.2, 5); // calls func1() using ptr Serial.println(a); ptr = func2; // resets ptr to point to func2() Serial.println(ptr(55.0, 5)); // calls func2() using ptr } int func1(float x, int y){ return y *= x; } int func2(float x, int y){ return x /y; }
The above code illustrates the creation of a function pointer and its use with two distinct functions where both have a similar return type and argument list.
The code declaring an array of function pointers should now feel a little more obvious.
void (*actions[3])(int, int);
That line would declare an array of three elements, each a pointer to a void function taking two int arguments. Here is a quick demo program that takes input from the Serial Monitor (valid values 0 to 2) and uses those to call one of three functions (incidentally passing two values collected from two Arduino analogue pins).
void (*actions[3])(int, int); // declares a function pointer array const int IN_1 = A0 ; const int IN_2 = A1 ; void setup() { Serial.begin(115200); actions[0] = actionA; // initialises the array elements actions[1] = actionB; actions[2] = actionC; } void loop() { int a = analogRead(IN_1); // will get a random value int b = analogRead(IN_2); // if pin not connected if(Serial.available()) { int act = Serial.parseInt(); actions[act](a, b); // calls a function selected from the // function pointer array } } void actionA(int temp, int sg) { Serial.println("Running action A"); // dummy actions } void actionB(int temp, int sg) { Serial.println("Running action B"); } void actionC(int temp, int sg) { Serial.println("Running action C"); }
This example could have been implemented using a switch or an if statement block. However, the flexibility of the function pointer should not be ignored. It would be perfectly possible to change the functions being pointed to as a process met new states. An array of function pointers can effectively be re-programmed while a program runs and circumstances change. They also deliver great performance.
As an example. A particular input may start a process set to continue for some time. You would probably not want to keep restarting that process if the input was repeated in the short term. So, you might change the function pointer to one that stops any further response. However, it might be that if the same input was received later but while the original long-term process should still have been running this signaled some sort of error state that should be escalated for attention – thus a third function (via it’s pointer) might be switched in to handle any such situation. Then finally, when the long running process had completed the original function pointer could be re-set, ready to run again. Such a process could potentially save many lines of code and simpler code results in a program that is easier to maintain as well as having fewer code errors.
Arrays of function pointers are also a way of implementing finite state machines and that could be very relevant to any process control application. A later chapter in this book tackles just such a project.
Continuing with the theme of flexibility, we should explore passing a function (or rather a pointer to a function) as an argument to a function. This technique can be used to alter the internal process of a given function without having to write multiple versions. Alternately, a function might be passed to another as an argument ready to be executed when the called function had completed (equivalent of a “call-back” function). In this alternate case, the same master function might be called from different locations within a program with different call-backs to finalise a process.
void setup() { Serial.begin(115200); Serial.println(math(21, 4, mod)); } int math(int a, int b, int (*mathOp)(int, int)) { return mathOp(a, b); } int sum(int a, int b) { return a + b; } int prod(int a, int b) { return a * b; } int divd(int a, int b) { return a / b; } int mod(int a, int b) { return a % b; }
The short demo program above gives you something to play with. The function math() accepts two int arguments and then a pointer to a function that returns an int and accepts two int arguments. You should hopefully now be comfortable with that function pointer definition. The second line of the setup() function can be changed to pass alternate int values to math and to change the arithmetic operation applied to those values – just by substituting the relevant function name (sum, prod, divd or mod). Give it a go!
[Don’t be tempted to rename divd() to div() as that will clash with a similar function defined in the stdlib.h library that is automatically included by the Arduino IDE.]
Functions that return a function pointer
A function returns a type, although that may be void. So, if we want a function to return a function pointer then we have to establish that function pointer as a type. The simplest way to do that is to use the typedef keyword:
typedef int (*fptr)(int);
defines a type that is a pointer to a function that returns an int and takes a single int argument.
We can then define a function that returns the type we just named fptr:
fptr myFun(int a) { … }
Here is some demonstration code:
typedef int (*fptr)(int); // defines the fptr function pointer type void setup() { Serial.begin(115200); fptr z = func(-4); Serial.println(z(3)); Serial.println(func(7)(4)); } int f1(int x) { return x * 23; } int f2(int x) { return x * 56; } fptr func(int a) { // defines a function that returns the fptr type if(a > 0) { return f1; // returns a pointer to f1 } return f2; // returns a pointer to f2 }
You will have noticed that the second Serial.println() statement in the setup() function above directly passes an argument to the function returned by func() for immediate evaluation. In this case the value 4.
Which just about completes our toolbox of techniques using function pointers.
Pointers can be used to hold the address of a variable, an array element, a function or even another pointer. The values and functions pointed to can be accessed using those pointers. The code generated by the compiler to support the pointer understands the relevant data type and manages the memory allocation required for each type.
The notation can seem complex at first glance and that can be exacerbated by code statements where the *s and &s are implicit. It helps to remember that an array name passed as an argument to a function is the name of a pointer (and not the array pointed to). Also, all function names are actually named pointers. Hopefully, you now know enough about pointers to decide which elements of a statement are pointers and which the variables they are pointing to.
Generic Pointers
So far, the pointers we have used have had a defined type. It is possible to define a generic pointer with a type of void. A void pointer type can point to any data variable except one that uses the const or volatile variable modifiers. A void pointer can be cast to any other pointer type without loss of data.
void setup() { Serial.begin(115200); int x = 99; void* vptr = &x; // defines void pointer to x int* iptr = (int*)vptr; // casts the void pointer to an int pointer byte* bptr = (byte*)vptr;// and to a byte pointer Serial.println(*iptr); Serial.println(*bptr); }
For the moment, just remember that you have met a void pointer and survived. You will meet them again later in the book when their utility can be properly demonstrated.
We will touch on pointers again when discussing structures (structs) and unions – oh and again even later when we tackle C++ classes.
Code downloads for this chapter are available here.