Pointers Made Easy Pt.1: The Basics

Pointers Made Easy Pt.1: The Basics

For most beginner programmers, the concept of pointers may seem daunting. Even so, pointers play a crucial role in understanding efficient memory allocation, complex data structures, interfacing hardware, and more.

I am making this series focused on pointers to help lessen the stigma surrounding pointers as well as encourage people to experiment and have fun with them.

What are Pointers?

Pointers are variables that store the address of other variables. They can alter the data stored inside a variable by simply referencing its address.

Pointers are declared with an asterisk (*) using the following format: datatype *ptr_name;

int main(void){
    int *iptr; //declaring pointers to multiple data types
    float *fptr;
    double *dptr;
/** 
 * We can also declare it as [datatype* ptr_name] e.g: int* ptr.
 * but this will not work for multiple single-line declaration.
 */
    int* ptr1, ptr2, ptr3; //only ptr1 is declared as a pointer
    int *ptr1, *ptr2, *ptr3; //ptr1, ptr2 and ptr3 are all declared as pointers
    return (0);
}

When we declare a pointer, we declare a pointer to a data type. What does this mean? Take int *ptr and int var for example. ptr is declared as a pointer to an integer data type and var is declared as an integer data type. On a 64-bit OS, ptr takes up 8 bytes in memory and var takes up 4 bytes in memory. This means these 2 are not the same data type! Remember that pointers store addresses and integers store integer-type data.

Initializing Pointers

There are 2 ways we can initialize pointers:

int main(void){
/****************** Method 1: At declaration *******************/

    int x = 5;
    int *ptr1 = &x; // &x denotes the address of x

/****************** Method 2: After declaration ***************/

    int y = 5;
    int *ptr2;
    ptr2 = &y; // when we assign address values to the pointer variable...
               //...outside declaration we do not include the asterisk (*)
    return (0);
}

Since pointers store the address of variables, when we assign an address value we use the ampersand (&) sign to get the address of a variable.

Accessing and Altering data stored at addresses using pointers:

When we want to access or alter data stored inside the address we are pointing to, we do what is called dereferencing the pointer. When we dereference a pointer, we use the asterisk(*) symbol:

int main(void)
{
    int x = 5;
    int *ptr;
    ptr = &x; //ptr points to address of x

    printf("value of x: %d\n", x);
    printf("the address at x: %p\n", &x);
    printf("the address stored inside ptr: %p\n", ptr);
    printf("*ptr accesses value at x: %d\n", *ptr);
    //dereferencing ptr = *ptr, to print x's value

    *ptr = 10; //Here we dereference ptr to change the value at x
    // Now x = 10

    printf("\nvalue of x: %d\n", x);
    printf("the address at x: %p\n", &x);
    printf("the address stored inside ptr: %p\n", ptr);
    printf("*ptr accesses value at x: %d\n", *ptr);
    return (0);
}
value of x: 5
the address at x: 0x7fffd741483c
the address stored inside ptr: 0x6fafe734423d
*ptr accesses value at x: 5

value of x: 10
the address at x: 0x7fffd741483c
the address stored inside ptr: 0x6fafe734423d
*ptr accesses value at x: 10

Notice that the address stored in ptr and the address of x are identical.

Pointer Use Cases: Pass by reference vs Pass by value

What is the difference between pass-by-reference and pass-by-value? You may come across these terms in multiple programming spaces.

Pass by value refers to passing a copy of a variable’s value as an argument of a function. The variable will only exist within the scope of the main function, whatever happens to the value passed to the external function does not affect the main function.

/**
* ch_val - function aims to change the value stored in variable
* @x: variable to be changed
* @change: value used to replace the value stored in `x`
*/

void ch_val(int x, int change)
{
    x = change;

    printf("the address of x in ch_val: %p\n", &x);
    printf("value of x in ch_val: %d\n", x);
}

int main(void)
{
    int x = 5;

    printf("the address of x in main: %p\n", &x);
    ch_val(x, 10);
    printf("value of x in main: %d\n", x);
    return (0);
}
the address of x in main: 0x7ffff93e6104
the address of x in ch_val: 0x7ffff93e60ec
value of x in ch_val: 10
value of x in main: 5

As you can see, the address of x in the main function and the address of x in the ch_valfunction is different. This is because even though they are both named ‘x’ they are two different variables existing only within the scope of their respective functions. Hence changing the value of ‘x’ within ch_val does not affect ‘x’ within main. Pass by reference is useful when we want to just return values to another function or write output to a file stream.

Pass by reference refers to passing a reference to a variable as an argument, instead of passing a copy of the variable’s value. Think of it passing the original variable instead of its copy. This is where pointers come in handy.

/**
* ch_val - function aims to change the value stored in variable
* @x: variable to be changed
* @change: value used to replace the value stored in `x`
*/

void ch_val(int *x, int change) // we use int *x instead of int x
{
    *x = change; //we dereference x to alter the value of x

    printf("the address stored in x pointer in ch_val: %p\n", x);
    printf("value of *x in ch_val: %d\n", *x);
}

int main(void)
{
    int x = 5;

    printf("the address of x in main: %p\n", &x);

   //passing address of x, we know pointers store the address of variables
    ch_val(&x, 10);

    printf("value of x in main: %d\n", x);
    return (0);
}
the address of x in main: 0x7ffff31424a4
the address stored in x pointer in ch_val: 0x7ffff31424a4
value of *x in ch_val: 10
value of x in main: 10

By looking at the output we can see that the address of ‘x’ in mainand the address stored in the ‘x pointer’ in ch_valare the same, this is how we know now we can alter the value of the original x variable in main.

Double Pointers

Unlike normal pointers, double pointers store the address of pointers, not a non-pointer data type. Double pointers are denoted by a double asterisk (**) with the format: datatype **ptr_name.

int main(void)
{
    int x = 5;
    int y = 25;
    int *ptr;
    int **dptr; //declaring double pointer (pointer to int *)

    ptr = &x;
    dptr = &ptr; //double pointer dptr stores the address of pointer ptr

    printf("value of x: %d\n", x);
    printf("the address at x: %p\n", &x);
    printf("the address stored inside ptr: %p\n", ptr);
    printf("*ptr accesses value at x: %d\n", *ptr);
    printf("address of ptr: %p\n", &ptr);
    printf("address stored of ptr stored in dptr: %p\n", dptr);
    printf("*dptr(equivalent to ptr) points to address of x: %p\n", *dptr);
    printf("**dptr(equivalent to *ptr) accesses value at x: %d\n", **dptr);

    **dptr = 10; //changes the value at x to 10;
    *dptr = &y; //changes where the pointer ptr points to
    //**dptr and *ptr now access the value at y which is 25
    return (0);
}
value of x: 5
the address at x: 0x6fafe734423d
the address stored inside ptr: 0x6fafe734423d
*ptr accesses value at x: 5
address of ptr: 0x7fffd741483c
address stored of ptr stored in dptr: 0x7fffd741483c
*dptr(equivalent to ptr) points to address of x: 0x6fafe734423d
**dptr(equivalent to *ptr) accesses value at x: 5

Let’s take the hypothetical situation above of a double pointer int **dptr pointing to a single pointer int *ptr. It’s important to note that when we dereference a double pointer once using*dptr (equivalent to ptr), we access the value stored in ptr (which is the address of x: &x). When we dereference a double pointer twice using**dptr, we then access the value stored in the address of x (5 being the value stored at that address). Quite a mouthful.

We can also go beyond double-pointers, such as triple-pointers and quadruple-pointers. But the use cases for applications beyond double pointers are very rare, with the understanding of how double pointers work it’s easy to formulate how other levels of pointers operate (e.g. triple pointers (***) point to the address of double pointers).

Stay Tuned for Pt.2, where we will be looking at pointers relating to strings and arrays.