Pointers and References


The subject of pointers usually deters students away. It is indeed tricky to grasp at first, but understanding it is essential when working with embedded systems.

By definition, a pointer can be considered a variable, but instead of storing a value, its purpose is to store an address of something. It points to the element location, not value.

A pointer can store the address of a variable, a function, a struct. Basically any element of the C/C++ language. If something has an address, a pointer can be used to point to the location of this thing.

We use the ampersand operator & to express the address of a variable.

When declaring a pointer, we use the star symbol to denote that, what we are declaring, is a pointer.

int a = 3;
int * p = &a; /* Declared pointer p and assigned address of a to it */

When we want to access the value of the pointer, we access the pointer directly just like a variable. Again, the pointer will give as an address.

int a = 3;
int * p = &a;
int * p2 = p; /* Address stored in p (address of variable a) given to a new pointer */

If we want to retrieve the value stored in the address, of what the pointer is pointing to, then we use the dereference operator *.

int a = 3;
int * p = &a;
int b = *p; /* The star here DEREFERENCES the pointer, retrieving the value in that address*/

So, heads-up, the star symbol is used in two different situations with pointers, in declaring a pointer and in DE-referencing a pointer.

int a = 3;
int * p = &a; /* star symbol for DECLARING a pointer */
int b = *p;  /* star symbol for DEREFERENCING a pointer */

Operation on Pointers

Here is an example

#include <iostream>

int main(int argc, char *argv[]){
    int a = 3;
    int *ptr = &a; /* initializing a variable with a * (star) prefix, declares it a pointer */
    
    std::cout << a << std::endl; /* Output: 3*/
    std::cout << ptr << std::endl; /* Output: 0x64E24FF7F0 (some random hex value) */
    /* derefrencing the ptr retrieves the value stored in the address pointed to by the pointer */
    std::cout << *ptr << std::endl; /* Output: 3, the * (star) prefix derefrences a pointer */
    return 0;
}
Output:
3
0x65fe1c
3

Here we create a regular variable.

int a = 3;

Then we create a pointer. The star symbol tells the compiler that this is a pointer. We define the ptr by assigning the address of the variable a, by using the reference operator.

int *ptr = &a;

When we print out the content of ptr, we will get a long hex number denoting an address in memory. On a PC. This will generally be a different number every time you compile and run the program.

std::cout << ptr << std::endl;

To print the content in the address inside ptr we use the star symbol to DEREFERENCE the ptr. We will basically get the value stored in the variable a

std::cout << *ptr << std::endl;

Pointer is a variable

#include <iostream>
using namespace std;

int main(int argc, char *argv[]){
    int x = 1; int y = 2; int *ptr;

    ptr = &x; /* Grabs the address of the variable x */
    cout << ptr << endl;/* Output: address of x */

    y = *ptr; /* Grabs the value in the address pointed to by ptr = value in x */
    cout << y << endl;  /* Output: 1 */

    x = (int)ptr; /* Grabs address pointed to by ptr and stores it in x */
    cout << hex << x << endl; /* Output: address of x as well */

    *ptr = 4; /* Changes the value stored in the location pointed to by ptr, changes the value of x */
    cout << x << endl; /*Output: 4 */
    return 0;
}
Output:
9
10

A pointer is a variable. We can do assignment and we can do math on it.

ptr = &x; /* Grabs the address of the variable x */

Here, we assign to y, the value stored in x by dereferencing the address of x.

y = *ptr; /* Grabs the value in the address pointed to by ptr = value in x */

Here we store in x, the address of x. So x stores the address of itself.

x = (int)ptr; /* Grabs address pointed to by ptr and stores it in x */

We can also change the value of x, by using the pointer. Since the pointer holds the address of x, we dereference ptr and assign a value to it.

*ptr = 4; /* Changes the value stored in the location pointed to by ptr: changes the value of x */

Incrementing Pointers

When incrementing a pointer, it will not necessarily be incremented by one. Instead, it will be incremented by the number of bytes of the data type it points to.

#include <iostream>

int main(int argc, char *argv[]){
    uint16_t a = 3;
    uint32_t b = 3;
    uint16_t *ptr_a = &a; 
    uint32_t *ptr_b = &b;

    std::cout << ptr_a << ", " << (++ptr_a) << std::endl; /* Ptr_a incremented by 2'  */
    std::cout << ptr_b << ", " << (++ptr_b) << std::endl; /* Ptr_b incremented by 4'  */
    return 0;
}
Output (address locations):
0x65fe0a, 0x65fe0c
0x65fe0c, 0x65fe10

For example, here we have two pointers: one for a 16bit variable and one for a 32bit.

When we increment the 16bit pointer, we jump two addresses. And when we increment the 32bit pointer, we jump 4 addresses. 8 to C in hex, that’s 4 decimal values.

Arrays vs. Pointers

Arrays and pointers are closely related.

#include <iostream>
using namespace std;

int main(int argc, char *argv[]){
    uint16_t sequence[5] = {2,4,6,8,16};

    cout << sequence[0] << endl; /* Output: 2 */
    cout << sequence    << endl; /* Output: a hex address */
    cout << *(sequence + 2) << endl; /*Output: 6, retrieving the 3rd array element */
    
    return 0;
}
Output:
2
0x65fe16
6

You can probably see that an array stores a sequence of values of similar data types; the array name is actually a pointer, pointing to the first element of the array.

You can usually treat the array name as a pointer.

Here we increment the pointer (the name of the array) by two, then dereference it. This wiill retrieve the 3rd array element.

cout << *(sequence + 2) << endl; /*Output: 6, retrieving the 3rd array element */

Passing Pointers

The strength of pointers is seen in one example by the way data can be passed between functions.

#include <iostream>
using namespace std;

/* A function accepts a ptr, a ptr is just a variable */
void sum_array(uint8_t *arr, size_t arr_s, uint16_t *sum_ptr);

int main(int argc, char *argv[]){
    uint8_t sequence[4] = {2, 3, 4, 1};
    uint16_t sum = 0; 
    /* When passing an array, need to pass size of array */
    sum_array(sequence,sizeof(sequence), &sum);
    cout << sum << endl;
    return 0;
}

void sum_array(uint8_t *arr, size_t arr_s, uint16_t *sum_ptr){
    for (int k = 0; k < arr_s; k++){
        *sum_ptr += arr[k]; /* The original variable is manipulated */
    }
}
10

When passing a variable to a function the usual way, what is passed is a copy of the variable. That’s extra time and memory consumed, especially if you are processing a large array.

Instead of passing a copy of the variable, we can pass the address of the variable. So the function can manipulate the original variable directly.

This efficient data handling is shown better when dealing with arrays. Instead of passing a copy of all array elements, we can pass a pointer to the array and let the function manipulate the array directly.

Note that arrays do not carry information regarding their size or length. So, along with the array name (which is a pointer), we have to tell the function the length of the array.

In C++, there are other abstract ways to store sequences of data using standard library templates such as lists, maps and vectors. That carry more information about themselves and embed functionalities to handle their data.

In this example, we have a function that sums array elements. It takes a pointer to the array, the size of the array and a ptr to the result variable.

/* A function accepts a ptr, a ptr is just a variable */
void sum_array(uint8_t *arr, size_t arr_s, uint16_t *sum_ptr);

Here we create the array, and a result variable. When calling the function, we pass the array name; which is a pointer, the size of the array and the address of the result variable.

int main(int argc, char *argv[]){
    uint8_t sequence[4] = {2, 3, 4, 1};
    uint16_t sum = 0; 
    /* When passing an array, need to pass size of array */
    sum_array(sequence,sizeof(sequence), &sum);
    cout << sum << endl;
    return 0;
}

Inside the function, we loop through the array elements and continue adding the values to the result variable. Note that sum_ptr is a pointer, so to manipulate the result variable we have to dereference it.

void sum_array(uint8_t *arr, size_t arr_s, uint16_t *sum_ptr){
    for (int k = 0; k < arr_s; k++){
        *sum_ptr += arr[k]; /* The original variable is manipulated */
    }
}

Next: Structs