Completed: / exercises

Pointers

Learning Objectives: After going through this material, you will know how to use pointers in C and understand the significant benefits of their use in memory management for low-level programming.
Let's start with a brief review. When we introduce a variable in our program, for example, int8_t x = 42;, the following happens:

Pointer Variables

Pointers are a way to reference memory locations used by variables in a program. Previously, we worked with variables directly by assigning and manipulating their values. With pointers, instead of handling values directly, we can store and reference the memory address where a value is located. This allows us to access and modify the value at that memory address, providing more control over the program’s memory.
Let's clarify this with a hypothetical example where we have introduced and initialized two variables in the program, a and addressof_a.
uint8_t a = 0x42;
uint8_t *addressof_a = &a; // &-operator
Okay, so far, there's nothing unfamiliar to us, two variables a and addressof_a, each with its own memory location (hypothetical memory addresses 0x8E and 0x8F):
Variable Type Variable Name Memory Address Value
uint8_t a 0x8E 0x42
uint8_t* addressof_a 0x8F 0x8E
Now, the difference arises in how the values of these variables are interpreted:
By examining the example, we see that the value of the pointer variable addressof_a is actually the memory address of the variable a! This is called pointing / referring to the variable a. Pointing means that, when using the pointer variable, you "jump" to the memory location indicated by its value. We can still handle the variable a as a "regular" variable; nothing changes in that regard.
Example. Let's see what happens in the following:
#include <stdio.h>
#include <inttypes.h> // Derived data types from here

int main() {
   uint8_t a = 0x42;
   uint8_t *addressof_a = &a; 
   
   printf("The value of variable a is %x\n",a);
   printf("Pointer addressof_a points to the value %x\n",*addressof_a);
   // Change the value of a with assignment
   a = 0x56;
   printf("Pointer addressof_a points to the value %x\n",*addressof_a);
   // Change the value of a through the pointer with assignment
   *addressof_a = 0x78;
   printf("The value of variable a is %x\n",a);
   
   return 0;
}
And this prints:
The value of variable a is 42
Pointer addressof_a points to the value 42
Pointer addressof_a points to the value 56
The value of variable a is 78
Now, pointer variables provide an indirect way to reference memory allocated for another variable. As we will see, this is extremely useful for writing more efficient and compact code, which is particularly important in embedded programming due to resource constraints. (Additionally, since the value of a pointer variable can be any number, we can point anywhere in the memory available to the program.)
We will spend the rest of this lecture explaining the benefits of such "sorcery".

Pointer Operators

But first, let's go over some C syntax. The language provides two operators for working with pointer variables. These operators can be used to determine the address of any variable, initialize pointer variables to a desired address, and retrieve the values of the pointed memory locations.

Operator &

The & operator (address-of operator) is used to query the memory address of any variable. The syntax for the operator is &variable_name. Let's look at a code example on the lecturer's tax-deductible home PC:
int8_t a = 12;
printf("The value of a is %d and the memory address is %p",a,&a); 
...which outputs, The value of a is 12 and the memory address is 000000000023FE47 (64-bit architecture on the processor).

Operator *

The * operator has three purposes.
1. A pointer variable is declared using the * operator, i.e., the syntax *variable_name.
int8_t *addressof_a = &a; 
ATTENTION: If you declare a pointer but it is not initialized, the value of the the pointer is undefined that is, garbage value. An attempt to dereference (access the value it points to) would lead to undefined behaviour
int main() {
    int8_t *pointer_a;  // Declared but not initialized
    printf("Uninitialized pointer value: %p\n", pointer_a);  // This could print any arbitrary address
    return 0;
}
Instead, if we want to not initialize a pointer when we define it, we should initialize to NULL
int8_t *pointer_a = NULL;  // Now pointer_a is explicitly initialized to NULL
2. The assignment operator * is used to assign a new value to the memory location of the (other) variable pointed to.
int8_t a = 47;
int8_t *addressof_a = &a;
*addressof_a = 23; // change the value of a through the pointer
Now, a is initialized to 47, and then a new value of 23 is assigned to it using the pointer addressof_a. Interesting...
In contrast, assignment without the * operator addressof_a = 23 would change the value of the pointer variable itself so that it would point to memory location 23 instead of (necessarily) the memory location of variable a.
3. The * operator is used with pointer variables to retrieve the value of the memory location pointed to by the pointer variable (dereference operator).
uint16_t x = 0xBEEF;
uint16_t *addressof_x = &x;	
printf("x=%x\n",*addressof_x); 
This prints the retrieved pointed values.
x=beef

Pointer Variable Type

We can observe from the above that a pointer variable is always given a data type when it is declared. But, why? Isn't it just a memory address?
The data type is needed so that the compiler knows what type of value the pointer is pointing to at that memory location. Let's look at the following code example.
int main() {
   uint32_t a = 0x12345678;

   uint8_t *pointer_byte = &a; // 8-bit pointer
   uint16_t *pointer_word = &a; // 16-bit pointer
   uint32_t *pointer_longword = &a; // 32-bit pointer
	
   printf("%x\n",*pointer_byte);
   printf("%lx\n",*pointer_word);
   printf("%lx\n",*pointer_longword);
	
   return 0;
}
In the code, a 32-bit integer variable is declared, and it is assigned 8-, 16-, and 32-bit pointers. When the value of the variable pointed to by each pointer is printed (on the lecturer's PC), the following output is produced:
78        // 8-bit pointer returns a byte
5678      // 16-bit pointer returns a word
12345678  // 32-bit pointer returns a long word
We observe that the compiler understands the pointer based on its data type and retrieves the corresponding value from memory! Therefore, it is crucial to declare the pointer variable with the same data type as the variable it points to.
Note! When compiling the example code, you might encounter a warning about declaring a pointer of the wrong type warning: initialization from incompatible pointer type.
Note! Why isn't the output 12 or 1234?? This is due to the processor architecture's byte order. -> Little endian in this case.

Pointer Variable in Memory

Of course, like all variables, a pointer variable requires its own memory location. Let's revisit the previous example code with some modifications. Notice the placeholder %p, which can be used to print memory addresses.
#include <stdio.h>
#include <inttypes.h>

int main() {
   uint8_t a = 0x12;
   uint8_t *pointer_a = &a;

   // new operator: sizeof	
   printf("Memory address of variable a: %p\n",&a);
   printf("Size of variable in bytes: %d\n",sizeof(a));
    
   printf("Memory address of the pointer variable: %p\n",&pointer_a);
   printf("Size of the pointer variable in bytes: %d\n",sizeof(pointer_a));

   return 0;
}
Which prints (with real memory addresses):
Memory address of variable a: 0x7ffd6232a93f
Size of variable in bytes: 1
Memory address of the pointer variable: 0x7ffd6232a940
Size of the pointer variable in bytes: 8
From the output of the example, we see that each pointer variable has its own address in memory, and using the newly introduced sizeof operator, we can get the size of the pointer variable's memory location (8 bytes -> 64-bit processor architecture). The essential thing to notice here is that the size of the pointer variable's memory location is 4 bytes, regardless of the fact that it points to a variable that is only 1 byte in size.

Using Pointers

Okay, great. But why do we need pointer variables?

Pointers and Array Variables

Let’s examine the close relationship between pointer variables and arrays.
Since pointers allow us to freely access memory allocated to the program, we can, of course, also initialize pointer variables to point to elements of an array.
char string[] = "XYZ";	
char *ptr_1 = &string[0]; // now points to 'X'
char *ptr_2 = &string[1]; // now points to 'Y'
char *ptr_3 = &string[2]; // now points to 'Z'
printf("%c%c%c\n", *ptr_1, *ptr_3, *ptr_2);
This prints:
XZY
There’s more interesting stuff when initializing pointers with arrays.
int main() {
   char string[] = "XYZ";	
   char *method_1 = &string[0]; // initialization method 1
   char *method_2 = string;     // initialization method 2. Using Array Decay

   // Print the string
   printf("%s\n", string);
   printf("%s\n", method_1);
   printf("%s\n", method_2);
	
   return 0;
}
Since all two initialization methods point to the same memory address, the string output is the same:
XYZ
XYZ
XYZ
We can observe that, in C, when an array is used in an expression, the name of the array decays into a pointer to its first element. This observation will be useful for us soon...
However, arrays and pointers are not identical; arrays are a block of contiguous memory, while a pointer holds the address of some memory location. Importantly, you cannot change an array's name to point to another location, unlike pointers, which can be reassigned.
Now, let’s consider what happens when you pass an array to a function. When you pass an array to a function, only the pointer to the first element is passed (array decay), so the function has no idea how large the array is unless you explicitly tell it.
Here’s an example of a function that prints an array of integers, and we must pass both the array and its size:
#include <stdio.h>

// Function prototype: Takes a pointer to an array and its size
void print_array(int *array, int size);

int main() {
    int numbers[] = {10, 20, 30, 40, 50};  // Declare an array of integers
    int size = sizeof(numbers) / sizeof(numbers[0]); // Calculate array size

    // Call the function with the array and its size
    print_array(numbers, size);

    return 0;
}

// Function to print array elements
void print_array(int *array, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
}
Attention: sizeof does not return the number of elements in an array but rather the total memory occupied by the array in bytes. To correctly calculate the number of elements, you should divide the total size of the array by the size of one element, like this:
int size = sizeof(numbers) / sizeof(numbers[0]);  // Correctly calculate the size here
You can always define the size of the array (e.g. a buffer) as a constant, and use it as an argument in size.

Pointer Arithmetic

Since the values of pointer variables are numerical values, we can, of course, manipulate them with all the arithmetic operations available in C.
For example:
int main() {
   char string[] = "ABCD";	
   char *ptr = string;

   // print each character in a loop..
   for (ptr = string; *ptr != 0; ptr++) {
      printf("%c",*ptr);
   }

   return 0;
}
The example code prints to the screen:
ABCD
Now is a good time to explain this example a bit:
What is remarkable is that arithmetic operations with pointers do not care about the variable type.
Example: In the following example, the ++ operator moves the pointer to the next element in the assignment statement ptr++.
#include <stdio.h>
#include <inttypes.h>

int main() {
   uint16_t array[] = { 0x1234, 0x5678, 0x9ABC };
   uint16_t *ptr = array; 

   for (ptr = array; *ptr != 0; ptr++) {
      printf("%lx ",*ptr);
   }
        
   return 0;
}
This prints:
1234 5678 9abc 
When you increment a pointer, the amount by which it increases depends on the size of the data type it points to. For example, incrementing a char * pointer moves it by 1 byte (since char is 1 byte), while incrementing a uint16_t * pointer moves it by 2 bytes (because uint16_t is 2 bytes).
This also highlights how C is truly a low-level hardware-oriented language. We can freely access the memory of our program and manipulate the values of variables and their memory addresses.
As a result, a C programmer must be very careful not to break anything while working with pointers. For example, we might accidentally point to a memory location outside the variable’s allocated memory space, inadvertently modifying another variable’s value (overflow), or even outside the memory space allocated for the entire embedded program, which could lead to crashes.

As Function Arguments

Just like any other variable type, pointers can also be used as function parameters. Essentially, the argument passed to the function is the memory address, which is stored in the function's local variable. This is extremely useful as we will soon see...
Let’s start with an example... the function swap below does not work. Why?
#include <stdio.h>

void swap(int8_t local_a, int8_t local_b); // prototype

int main() {
   int8_t a = 14;
   int8_t b = 68;
   printf("Before: a=%d and b=%d\n", a, b);
   swap(a, b);
   printf("After: a=%d and b=%d\n", a, b);
}

void swap(int8_t local_a, int8_t local_b) {
   int8_t temp = local_a;
   local_a = local_b;
   local_b = temp;
}
This prints:
Before: a=14 and b=68
After: a=14 and b=68
The explanation for this can be found in the Functions in C material from earlier lectures. We remember that in C, a function creates copies of the arguments in local variables. In the above code, we are only swapping the values of the copies (the variables local_a and local_b), not the values of the original variables a and b.
No worries. This issue can be fixed by using pointers as function arguments. Now, in the function, the pointer local_a points to the memory address of a passed as an argument.
#include <stdio.h>

void swap(int8_t *a, int8_t *b); // pointer prototype

int main() {
   int8_t a = 14;
   int8_t b = 68;

   printf("Before: a=%d and b=%d\n", a, b);
   swap(&a, &b); // Note the use of the & operator
   printf("After: a=%d and b=%d\n", a, b);
}

void swap(int8_t *local_a, int8_t *local_b) {
   int8_t temp = *local_a; // save the value pointed to by a
   *local_a = *local_b;    // set the value pointed to by a to the value of b
   *local_b = temp;        // set the value of b to the original value of a
}
Now the program prints:
Before: main_a=14 and main_b=68
After: main_a=68 and main_b=14

Memory Savings

In the same way, pointers can also significantly save memory!
Let’s recall an example from earlier material, where we passed a struct with a large array as an argument to a function, and the poor function tried to obediently make a copy of it each time.
// Define a struct for the message
typedef struct {
    char destination_address[4];  // 4-byte address
    char message[2048];           // 2-kilobyte message
} Message;

...
// Declare a message struct and fill it with data
Message message_home = {"ABCD", "This is the message content..."};
...
// Call the function with the struct as an argument (passing by reference)
send_message(message_home);
...
This issue can be conveniently fixed by passing the address of the struct as an argument:
// Define a struct for the message
typedef struct {
    char destination_address[4];  // 4-byte address
    char message[2048];           // 2-kilobyte message
} Message;

// Function prototype
void send_message(Message *message_home);

int main() {
    // Declare a message struct and fill it with data
    Message message_home = {"ABCD", "This is the message content..."};
    
    // Call the function with the struct as an argument (passing by reference)
    send_message(&message_home);

    return 0;
}
Note! Of course, the function makes a copy of the pointer variable, but a pointer type (which is 8 bytes in size) is much smaller than the array itself.

Returning from a Function

Let’s return briefly to the previously discussed swap function, as it reveals another interesting thing...
void swap(int8_t *a, int8_t *b) {
   int8_t temp = *a;
   *a = *b;
   *b = temp;
}
In this function, we’re actually returning two values as the result of the function’s execution, namely the swapped values of variables a and b. In this way, we circumvent C’s limitations by directly manipulating memory instead of the variables themselves. Super convenient!
Note! We can also return a memory address from a function. But, make sure the pointer refers to valid memory (e.g., dynamically allocated memory or a global variable). Returning a pointer to a local variable in a function is unsafe, as the memory becomes invalid once the function exits.

String Handling

One of the key benefits of pointers is evident in handling strings.
As part of the standard library, the library string.h defines a set of useful functions for working with strings:
#include <stdio.h>
#include <string.h>

int main() {

  char name[] = "Judge Dredd";
  printf("The length of the name %s is %d characters\n", name, strlen(name));

  return 0;
}
All these library functions accept strings as pointers through their variable names. In other words, they assume that the given array always ends with a null character and therefore do not require the size of the array as a parameter. This, of course, can be dangerous if you are working with strings that do not end with a null character. So be careful!!!

Convenience for Low-Level Programming

Finally, here's an example of the strtok function, because it is extremely useful in embedded systems.
As noted earlier, data structures and wireless messages often follow the CSV format, where different fields in the message are separated by commas.
For example: 1234567890,temperature,27,C, where the first field is the timestamp (i.e., when the sensor value was measured), followed by the sensor type (temperature), measurement value (27), and measurement unit (Celsius).
Now, breaking down such a CSV-formatted string into parts is easily done with the strtok function.
#include <string.h>
#include <stdio.h>

int main () {
  char str[] = "1234567890,temperature,27,C"; 
  const char sep[] = ",";  // Separator is a comma
  char *token; // Placeholder pointer
   
  // Extract the first part of the message
  token = strtok(str, sep);
   
  // Loop to extract the remaining parts
  while( token != NULL ) {
    printf("%s\n",token);

    // Call the function again, continue from the placeholder
    token = strtok(NULL, sep);
    }
   
  return(0);
}
Let’s see what the example prints!
1234567890
temperature
27
C
Note: strok does not work in an intiutive way. You can find further explanation on how it works here
In the example, it's important to note that the separated parts are still strings. To process the numerical values in the message as numbers, we need to convert the strings to integer or floating-point variable types.
For this, the standard library stdlib.h provides functions such as atoi for integers, atol for long integers, and atof for floating-point numbers. Some of these functions are based on older C standards, but we can still use them. Modern system uses strtol and strtod.
#include <stdio.h>
#include <stdlib.h>

int main () {
  char str[] = "1234567890"; 
  
  long value = atol(str);
  printf("%ld\n",value);
   
  return(0);
}
However, in embedded development environments, these standard library functions might be replaced by other functions, or there might be limitations in their implementation.

In Conclusion

Help
This material provided a sufficient introduction to pointers in C for the course. They have more secrets... such as command line arguments, (multi-dimensional) pointer arrays, and function pointers, which we won't cover in the course. For those interested in learning more about pointers, additional information can be found in textbooks.
?