C basics: pointers

Pointers are a very powerful feature of C language, but they can be dangerous if misused or misunderstood. In this article I am going to shed some light on what are pointers and how to properly use them!

Pointers are Addresses

The first and most important thing to remember is that pointers are addresses. Differently than an ordinary variable, which stores data, pointers are used to store addresses or references to other data. This is why sometimes one might see the terms referencing and de-referencing when dealing with pointers.

Referencing is nothing else than getting a reference to a variable (let’s focus on pointers to variables for now). The reference in this case is the memory address where the variable is stored. We can use the ampersand operator (&) to get the address of a given object. So if we had a variable named “myVariable”, by writing &myVariable we could get the address of myVariable in memory.

The opposite operation of referencing is de-referencing and it means accessing the data stored in a given address. De-referencing in C is performed by the asterisk (*) operator, hence, by writing *myAddress we can access the data “pointed to” or “referenced by” myAddress. Note that * expects a pointer variable as its unary operand.

A pointer can also be stored in the form of a variable, to declare a pointer variable we also use the asterisk (*) operator. That said, by writing  “int varX” we declare a variable named varX of type int, whereas writing “int * varY” declares a variable named varY which is a pointer to an integer.

Now, what is the difference between these two variables and what are the similarities?

  • Both varX and varY are stored in memory;
  • varX stores data (int type);
  • varY stores an address that refers to int data;
  • One can get the address of either varX or varY by using & operator: &varX or &varY;
  • One can get the data pointed to by varY by using the * operator: *varY (you can’t use *varX as varX is not a pointer.

Figure 1 below tries to demonstrate what we said above. Considering that our variables are stored starting from memory address 1000, we can see that varX (memory address 1000) holds value “10” and varY (memory address 1001) holds value 1000, which is the memory address of varX! Both varX and varY are variables, but varX stores data whereas varY stores addresses (in this case 1000 which is the address of varX).

Figure 1

Pointers are cool aren’t them? But they can also be dangerous: pointer misuse can lead to catastrophic failures! And why is that?  Let’s first consider an ordinary uninitialized variable: accessing such a variable will most probably result in a wrong value but your application will continue to run and might be able to recover from it. But accessing an uninitialized pointer can cause a program to crash (segmentation fault, hardware fault or else, depending on the environment), this is because the processor might try to read/write to an address that might not belong to your application, that is not mapped to physical memory or that can’t perform the desired access method (such as trying to write to a flash memory position).

That said, the first golden rule of pointers is: always check the pointer before using it! Of course it is not easy to know if the memory address is valid or accessible, but the usual method is to set uninitialized pointers to NULL and check for NULL before using them.

First Example

It is time for our first example, let’s take a look at a this very simple code:

Let’s check its output:

myInt = 200
myInt address = 0x7ffdecde0ff4
myIntPointer referenced address = 0x7ffdecde0ff4
myIntPointer referenced data = 200
myIntPointer address = 0x7ffdecde0ff8
myInt = 201
myIntPointer referenced address = 0x7ffeedd10da4
myIntPointer referenced data = 201

Looking at the output we can see that myInt is stored at address 0x7ffdecde0ff4. In the beginning of our program we assign the address of myInt to myIntPointer (line 11). This means that myIntPointer is now pointing to (or referencing) myInt. The reader can see that in the output, since “myIntPointer referenced address” and “myInt address” show the same address.

Note that the address of myIntPointer is different: 0x7ffdecde0ff8. This is because myIntPointer is a variable on itself and it is stored in that memory address.

It is important to highlight that these addresses we see are very big numbers aren’t them? This is because the target machine (the one running the code) is a 64-bit x86 machine. Had we compiled the same code to a 32-bit architecture, we would see 32-bit addresses and an 8-bit or 16-bit architecture would most probably use a 16-bit address.

Now back to our code, line 18 (*myIntPointer)++ de-references myIntPointer (meaning that it gets the data that is stored in the address pointed by myIntPointer) and increments it. This operation effectively increments the contents of address 0x7ffdecde0ff4, which is where myInt is stored. That means that we have just incremented myInt indirectly by using a pointer!

That is confirmed when we print out the contents of myInt, which now shows 201 (it was 200 before).

Pointers to Anything

In C it is possible to get pointers to almost anything: arithmetic types (char, int, long, float, etc), other pointers (called multiple indirection), unions, aggregate types (such as arrays and structures) and functions. GCC also includes an extension that allows getting pointers to labels using the operator && (see my GPL FSM article).

While all these different pointers might look the same, since they all store addresses, the truth is that pointers to different objects might behave differently, according to the memory alignment of the object they are referencing. In other words, a pointer to a byte-sized object (char or uint8_t) must be byte-aligned so that it always points to a valid byte (which is easy), a pointer to a short int (usually uint16_t) must be 16-bit aligned and so on.

Assigning pointers must always consider these alignment constraints so that a pointer always points to a valid memory address (and possibly a valid object).

But there are situations when you need to have a pointer that can reference different objects with different alignments. One way to do that is to cast the different pointers to a common single pointer type, but C has a more elegant solution: void pointers (or void *).

They are a special class of pointers which does not have any data type associated with them, thus they can reference anything! Void pointers can be assigned with any other valid pointer type and vice-versa! In terms of implementation, void pointers follow the same rules as char pointers.

Consider the following example:

We can see that px (a void pointer variable) receives the address of va (an int pointer variable which holds 0x1234) and then px is assigned to p2 (a char pointer variable), when we print the contents of the referenced address, we see 34 (0x34) as the result. Of course you could directly assign the p1 to p2, but that would give you a warning since those are pointers to different types. You could also cast p1 into a char pointer: p2 = (char *)p1 but the idea here is just demonstrate that void pointers can point to anything!

Note: assignments between different pointer types can lead to catastrophic failures (such as a bus fault) if done incorrectly and object memory alignment is not obeyed.

Pointer Operations

Because of their very special nature (pointers are addresses), there are a few operations that are allowed on pointers: assignment, increment, decrement, summation and subtraction. You can also compare pointers (for equality, higher or less than). Void pointers are even more restrictive and allow only assignment and comparison.

One important thing to consider when performing an operation on a pointer is that the operation will always take into account the type of the pointer (what kind of object the pointer is referencing). When you add two to a pointer you are effectively stating that you want the pointer to reference an object that is two “object sizes” ahead. That means that on most architectures these operations will use the following offset for each object:

  • char or int8_t – offset of 1;
  • int or int16_t – offset of 2;
  • long, int32_t or float – offset of 4;
  • long long or int64_t or double – offset of 8;
  • unions – offset is the size of the largest object within the union;
  • structures – offset is the size of structure;

Note: GCC has an extension that will consider void pointers and function pointers as char pointers, so incrementing a void pointer or function pointer in GCC will add 1 to it! Using option -Wpointer-arith will issue a warning if such an operation occurs on a void or function pointer.

Why would I use pointers?

There are several situations where you would want or need to use pointers, let’s take a look at some of them:

1- To pass parameters to a function by reference

Passing values to a function is usually done by value, meaning that the argument need by the function is copied somewhere (either CPU registers or the stack, depending on the architecture and its calling convention). But sometimes you expect the function to perform operations on the data, modifying the actual variable itself and not a copy of it. The classical example is the scanf function, which expects a pointer to the variable which is gonna store the data read from the input device. How can we do that? Pointers!

The reader might say: oh, when I want a function to alter a variable I just return the new value! Yes, you can pass an argument by value, process it inside the function and return an updated value, that is true, but what happens when you need to change multiple values? C does not allow returning multiple values (unless you use a struct, which has its own implications as we will see in another article).

This would be a use case for pointers! Let’s take a look at another example that easily demonstrates this. Suppose you wanted to compute the minimum and maximum values found within a given array of integers. The following code demonstrates two ways to do that: by using two separate functions (one for minimum and another for maximum) and by using a single function that receives pointers as arguments. Let’s take a look!

We are not gonna spend too much time explaining how it works (it is honestly very simple), but let’s take a quick look on how we used pointers to write getMinMax:

The reader can see that we declared three pointer parameters: arr, min and max. The first one is interesting: in C arrays and pointers are interchangeable. Referencing an array by its name (without an index) results in a pointer to the array (in fact a pointer to the first element of the array). The other two (min and max) are pointers to integer variables. All these three parameters are said to be references. That means that a reference (pointer) is passed to the function, not their actual value. That also means that the function will be able to modify the content of these variables!

Since we are using pointers for some function parameters, we need to call the function appropriately. That means using pointers for the appropriate arguments when calling the function:

Note that we don’t need to use & for data (since we want to pass a pointer to the array). This is because, as we said, an array without index is a pointer to the first of its elements! We could also write this:

Either way we are passing a pointer to the array (a reference), the size of the array (a value), a pointer to the minimum variable (a reference) and another pointer to the maximum variable (also a reference). That means that all computation performed by getMinMax will actually happen on data, min and max. In this casa data is a global variable and min and max are local variables within main() scope.

2- To use dynamic allocation

While dynamic allocation is not really advised when working on embedded systems, there are several situations (especially on more complex systems) where using dynamic allocation (usually malloc, calloc or a memory allocation function provided by an RTOS) is the best option.

In these cases, there is no way to avoid pointers since memory allocation functions usually return a pointer to the allocated area.

3- To design complex data structures

Implementing data structures such as linked lists, trees, hash tables and others makes use of pointers in most cases. These data structures are usually dynamic allocated and need to make use of pointers in order to handle each node/entry.

4- To access variables outside of their original scope

Accessing a variable outside its scope is usually not possible in C (well, in most languages I guess). This is because variables declared inside a scope are private and should not be visible to other functions/modules. Scope in this sense can be any block of code within curly brackets. But what if one needs to expose one of those variables? Yes, pointers!

Since a pointer is a reference to a memory address, it is not restricted by scope mechanisms and is able to access any memory allocated to the application. Note that modern operating systems running on hardware with MMU (Memory Management Unit) or MPU (Memory Protection Unit) will prevent an application from accessing memory that doesn’t belong to it (meaning other applications or the OS kernel)!

5- To access hardware registers

When writing software for embedded systems it is usual to directly  access hardware registers in order to control peripherals. But how can one map a variable to a fixed memory address? Yes, pointers!

Looking at ST’s STM32 HAL headers, it is easy to see how they use pointers to map I/O structures to specific memory addresses:

#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)

So GPIOA is pointer to a GPIO_Typedef located at GPIOA_BASE. If we follow the trail:

#define GPIOA_BASE (AHB2PERIPH_BASE + 0x0000UL)

AHB2PERIPEH_BASE maps to:

#define AHB2PERIPH_BASE (PERIPH_BASE + 0x08000000UL)

PERIPH_BASE maps to:

#define PERIPH_BASE (0x40000000UL) /*!< Peripheral base address *

That means our GPIOA structure maps to address 0x48000000 which is the base address for GPIOA on a STM32L432! Cool isn’t it? All thanks to pointers!

6- To get the address of a function

Some applications might need to dynamically assign a function to work as a callback. That means that a specific action or trigger fires a specific function. Think of a command processor, where each command runs a different function to perform the desired task.

Task schedulers (such as my preemptive scheduler ULWOS and my cooperative thread scheduler ULWOS2) rely on function pointers to dynamically assign the function address (task or thread to run) to a variable that controls which task should run next.

These are examples of function pointers, another kind of pointer that points to code instead of data. We will talk more about function pointers in an upcoming article.

Closing

I hope this article helped clarifying a little bit what are pointers, how they work and why to use them. Let me know if you think there is something else that should be here!

Leave a Reply

Your email address will not be published. Required fields are marked *

1 × 5 =