It’s been a while since the previous part of this series. This edition will take a side-step away from the regular Arduino/C++ and Python examples and look at the language C, which is quite similar to the Arduino/C++ language.
This time we will look at something called pointers, which might be a step up in complexity from the topics we’ve touched upon earlier, but we’ll try to make it as digestible as possible. We’ll also look at an example which uses pointers to dynamically allocate memory.
Here’s a list over the previous editions in this series:
- Part 1: Skimming the Surface
- Part 2: Conditional Statements
- Part 3: Loops
- Part 4: Arrays and Lists
- Part 5: Functions and Scope
- Part 6: Defines and Random Numbers
The C Language
C is the “predecessor” to C++ which the Arduino language is based upon. C, being a relatively old language, is still a very widely used language, especially as a firmware language in the majority of all microcontrollers.
Depending on who you ask, C is a low-level or a mid-level language. With level we don’t mean how “good” the language is or anything along those lines, but rather how close to the hardware it is, where lower is closer. Python is a typical language which is on a higher level than C.
In C you don’t have anything happening under the surface, as opposed to Python, for instance. What you see is what you get.
You rarely use pointers while programming Arduinos and they don’t explicitly exist in Python, so to properly examplify this topic we need to look at a different language. The reason we’re choosing C to look at pointers is that we feel this is a relevant language for makers due to its close relation to microcontrollers.
Pointers
Pointers are in essence variables that refer to other variables. Immediately, this might seem pointless (no pun intended), but in many situations pointers will make your life easier, and in others pointers are plain necessary. Let’s start with a trivial example:
#include <stdio.h> int main() { int my_number = 4; int *my_pointer; my_pointer = &my_number; printf("Number: %d Pointer: %d\n", my_number, *my_pointer); return 0; }
This code will print out Number: 4 Pointer: 4
to console.
- First, we declare the integer variable
my_number
and set it to 4. - Then we declare the integer pointer (aka. int pointer) called
my_pointer
. Notice the asterisk in front of the variable name. This is what makes this a pointer. Theint
part defines the type of pointer, i.e. what variable type it will point to. - On the third line in the main function we set
my_pointer
to point tomy_number
. By writing an ampersand (&
) in front of a variable name (regardless of working with pointers or not), you will get the memory address to that variable. And this is an important part of this topic: the value of a pointer variable is the memory address of the variable it points to. To reiterate: the value ofmy_pointer
ismy_number
‘s memory address. All variables have a unique memory address. - The
printf
function is a standard function in C/C++ which lets you print stuff to console and has nothing to do with the scope of this post directly. If you want to know more, see this page. Something important that needs to be explained is the part*my_pointer
. In this case, the use of an asterisk dereferences the pointer. This means that it returns the value of the variable it points to, in this the value ofmy_number
which is 4.
You can of course combine the second and the third line in the main function like this: int *my_pointer = &my_number;
.
We know that this might be challenging to wrap ones head around, so we’ll try explaining this in an additional manner:
my_variable
will give us 4.&my_variable
will give us 0x8ce7b38c.my_pointer
will also give us 0x8ce7b38c.*my_pointer
will give us 4.&my_pointer
will give us 0x8ce7b380.
If we for instance write *my_pointer = 5;
after we’ve set my_pointer
to point to my_variable
, we will change the value of my_variable
to 5 without even mentioning it!
Let’s use pointers in a somewhat more practical example.
The Similarities Between Arrays and Pointers
In C, arrays and pointers are practically the same (roughly speaking). However, if you want to take arrays as input parameters to functions or return arrays, you have to use pointers. An array is actually a pointer pointing to the first element of the array. This explains why we can do what we do in the code below:
#include <stdio.h> #include <stdlib.h> #include <time.h> #define BUFFER_SIZE 8 void fill_buffer(int * buffer, int size); void print_buffer(int * buffer, int size); int main() { int my_buffer[BUFFER_SIZE] = {0}; srand(time(NULL)); //set random seed fill_buffer(my_buffer, BUFFER_SIZE); print_buffer(my_buffer, BUFFER_SIZE); return 0; } void fill_buffer(int * buffer, int size) { int i; for(i=0; i<size; i++) { buffer[i] = rand()%100; //insert a random number between 0 and 99 } } void print_buffer(int * buffer, int size) { int i; for(i=0; i<size; i++) { printf("%d ",buffer[i]); } printf("\n"); }
A few comments on the code above:
- We declare
my_buffer
as an array, but the two functions take int pointers as input parameters. We can’t writevoid fill_buffer(int buffer[]);
or something along those lines! - The size of an array has to be constant, hence the
#define BUFFER_SIZE
. When creating arrays like this, the compiler has to be sure that the size of the array won’t change during runtime. - We can create dynamically sized buffers by using a function called
malloc()
. This function allocates memory during runtime. Usingmalloc()
is genererally not recommended while programming microcontrollers. There are many reasons for this such as the lack of memory management.
Malloc()
Here’s a similar example where we dynamically allocate memory using malloc()
:
#include <stdio.h> #include <stdlib.h> #include <time.h> void fill_buffer(int * buffer, int size); void print_buffer(int * buffer, int size); int main() { srand(time(NULL)); //set random seed int buffer_size = (rand()%10) + 1; //random buffer_size between 1 and 10 int *my_buffer = (int *) malloc(buffer_size*(sizeof(int))); //allocate memory to buffer fill_buffer(my_buffer, buffer_size); print_buffer(my_buffer, buffer_size); return 0; } void fill_buffer(int * buffer, int size) { int i; for(i=0; i<size; i++) { buffer[i] = rand()%100; //insert a random number between 0 and 99 } } void print_buffer(int * buffer, int size) { int i; for(i=0; i<size; i++) { printf("%d ",buffer[i]); } printf("\n"); }
Notice the lack of #define
and traditional array declaration. Instead, we set the buffer size to a random length and keep my_buffer
as an int pointer.
malloc()
returns a void pointer, so we have to cast it to an int pointer with (int *)
. As an input parameter we need to specify how much memory we need to allocate. Here we take the number of elements multiplied with the size of each element which we get from sizeof(int)
since this is a buffer with ints. If we needed a char buffer instead, we’d write char *my_buffer = (char *) malloc(buffer_size*(sizeof(char)));
. This would result in less memory allocation since one char takes up less memory than an int.
Summary
It is difficult to find practical examples for pointer usage without either making things more complex (for instance linked lists) or going into too specific scenarios for the purpose of this tutorial. We hope that this tutorial is helpful nontheless. You’ll bump into pointer sooner or later in C anyway, so knowing your pointer syntax is important.
Pointer syntax summary:
char *bob;
creates a char pointer namedbob
.bob = &samurai;
sets thebob
pointer to point to a variable namedsamurai
, given the line above.*bob = 's';
setssamurai
tos
, given the two lines above.
In later parts of this series we’ll look into more practical use of pointers.
If you want to easily practice coding yourself, we recommend this page which has enviroments for both C, C++ and Python as well as several other languages.