Pictured is a peer learning day occuring

 

Pointers in C 


I, of course, committed to Holberton School, a non-traditional path, to pursue my dream of software engineering. Yet, I will always be thankful for the professor of an introductory C programming class I took while still an undergraduate at UC Davis, and will always remember how he opened his first lecture on pointers:

Pointers have a reputation for being scary. But pointers do not have to be scary. Pointers are not scary. I do not want you to be scared of pointers.

It is easy to be initially intimidated by pointers. Basic variable declarations in C are relatively intuitive - declare a type, specify a variable name, and define values directly using an equal (=) sign. Yet, pointers introduce an entirely different syntax that can appear strange at first - where in the world did the multiplication sign come from? And why is it being used on both sides of the equal sign?

Trust me, I’ve been there. I’ve been in your shoes, and felt that the syntax behind pointers only introduced confusion and frustration. For this same reason, I want you to take my word for it - my professor was correct. Pointers do not have to be scary!

Although the syntax surrounding pointers initially comes off as thrown out of left field, it is nothing more than just that - syntax - and it is designed to simply express a powerful and useful concept of the C language. Today, I would like to follow in my professor’s footsteps. In fact, I am going to one-up him. I do not just want you to be unafraid of pointers - I want pointers to be your friend.

POINTERS - WHAT

In its most basic description, a pointer is a memory address. Nothing else to it! I promised you pointers are not scary, didn’t I 😉.

Of course, that is not technical enough for us. Allow me to clarify what exactly I mean by memory addresses.

Computers store digital information in the form of bits and bytes, with one byte representing the equivalent of eight bits, and one bit representing two possible values - true (1) or false (0). Memory is tracked using what is referred to as addressing, the designation of bytes with numerical values; most commonly, addresses are represented in hexadecimal values.

To visualize memory addressing, think of an array of bits representing an increasing range of values.

Address 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
Value Range (bits) 0 0-3 0-7 0-15 0-31 0-63 0-127 0-255

The above table represents eight bits which combined span the potential value range of 0-255 (2 values/bit ^ 8 bits = 256 possible values). More succinctly and relevantly, we can represent this memory in the format of bytes (eight bits), with one hex address representing one byte.

Address 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
Value Range (bytes) 0-255 0-255 0-255 0-255 0-255 0-255 0-255 0-255

Now, reintroducing pointers - a pointer is a block of memory that refers to another memory address.

On 64-bit machines, pointers take up 8 bytes of memory (on 32-bit machines, they take up 4 bytes). This can be proven using the C standard library sizeof operator. I am going to begin using pointer syntax for exemplary purposes, but don’t worry, I will go into detail on usage soon.

$ cat main.c
#include <stdio.h>

int main(void)
{
        printf("The size of a pointer is %ld bytes!\n", sizeof(int *));
        return (0);
}
$ gcc main.c -o size
$ ./size
The size of a pointer is 8 bytes!

[Note the additional use of the function printf here, another C standard library function that prints text to standard output, as well as the standard library macro EXIT_SUCCESS, a return value for indicating successful function completion.]

In the above, I exemplified a pointer to an integer, but pointers can refer to any C data type.

$ cat main.c
#include <stdio.h>

int main(void)
{
    printf("The size of an int pointer is %ld bytes!\n", sizeof(char *));
    printf("The size of a char pointer is %ld bytes!\n", sizeof(int *));
    printf("The size of a short pointer is %ld bytes!\n", sizeof(short *));
    printf("The size of a long pointer is %ld bytes!\n", sizeof(long *));
    printf("The size of a float pointer is %ld bytes!\n", sizeof(float *));
    printf("The size of a double pointer is %ld bytes!\n", sizeof(double *));
    printf("The size of a void pointer is %ld bytes!\n", sizeof(void *));
    return (0);
}
$ gcc main.c -o size
$ ./size
The size of an int pointer is 8 bytes!
The size of a char pointer is 8 bytes!
The size of a short pointer is 8 bytes!
The size of a long pointer is 8 bytes!
The size of a float pointer is 8 bytes!
The size of a double pointer is 8 bytes!
The size of a void pointer is 8 bytes!

The above prints the size of eight different pointers referring to eight different data types, but the size of each remains constant at, well, eight bytes. In other words, pointers are no more than 8-byte [on 64-bit machine] blocks of memory that refer to the memory address of something, anything, else.

Aside: Why 8 bytes?
Why do pointers take up 8 bytes on 64-bit computers, you ask? Thank you for clarifying the computer architecture. The 8-byte count taken up by pointers is crucially exclusive to 64-bit machines, and for a reason - 8 bytes is the largest possible address size available on that architecture. Since one byte is equal to eight bits, 64 bits / 8 = 8 represents the size of a pointer. On 32-bit machines, pointers correspondingly take up 32 bits / 8 = 4 bytes.

POINTERS - HOW

I gave you a sneak peak in the examples above, but pointers can be declared in C using the asterisk (*) character. Since C is a statically-typed language (ie. variables are typed up-front), pointers must always be declared with the data type they will refer to.

For instance, you can declare a pointer to an int as follows:

int *ptr;

And a pointer to a char:

char *ptr;

And a pointer to a float:

float *ptr;

... I’ll stop there.

In any of the above, when we declare the variable ptr, our [64-bit] computer sets aside 8 bytes of memory. Initially, since we have yet to define the variable, this 8 bytes refers to nothing. Here’s a visualization, using imaginary hexadecimal values to represent our memory addresses.

Address 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
Variable ptr
Value ?

So we have a pointer! But, is a pointer really a pointer if it doesn’t actually point to anything? Well, I guess so. A pointer is ultimately just a block of memory, after all. But let’s go ahead and make our pointer useful by referring it to a memory address.

First, we’ll define an integer for our pointer to refer to.

int num = 7;

In defining this integer num, we allocate a new value in memory (four bytes for ints). Let’s say that our integer is randomly allocated to the hex address 0x20.

Address 0x20 0x21 0x22 0x23
Variable num
Value 7

Now, we can refer our pointer to the memory address of the variable num.

int num = 7;
int *ptr = #

Notice the use of the & character. The ampersand is a special character in C used to access the memory address of a variable - this can be proven using the printf conversion specifier p, which outputs a memory address in hexadecimal format.

$ cat main.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int num = 7;

    printf("Ampersands access memory addresses! See! -> %p\n", &num);
    return (EXIT_SUCCESS);
}
$ gcc main.c -o ampersand
$ ./ampersand
Ampersands access memory addresses! See! -> 0x7ffd2766a944 

Why must we store the address of num, instead of pointing ptr directly at it? Good question. Remember that pointers specifically refer to memory addresses. In our working example, we are interested in making ptr refer not to the value of num, but to its memory address 0x20 - thus, we must use the ampersand character.

Updating our tables, here’s what our memory looks like now.

Address 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
Variable ptr
Value 0x20
Address 0x20 0x21 0x22 0x23
Variable num
Value 7

Cool! ...so now what?

Well, now that we’ve referred ptr to the memory address of num, we can use the dereferencing character, asterisk (*), to access the value located at that address.

$ cat main.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int num = 7;
    int *ptr = #

    printf("The pointer ptr refers to a memory address containing the value %d\n", *ptr);
    return (EXIT_SUCCESS);
}
$ gcc main.c -o pointer
$ ./pointer
The pointer ptr refers to a memory address containing the value 7.

Aside: Haven’t we used the asterisk before, to declare pointers? How are we using the same character here to dereference them?
Good eye. The asterisk character has dual meaning when it comes to pointers - it is used both to declare and dereference them. If you ever get mixed up, remember that pointers must always be declared on a data type, so an asterisk used on a variable by itself will only ever mean it is being used for dereferencing.

Beyond merely accessing values, the dereferencing character can be used to change the value located at whatever memory address a given pointer refers to. For instance, now that we’ve referred our pointer ptr to the memory address allocated by num, we can actually change the value stored there.

$ cat main.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int num = 7;
    int *ptr = #

    printf("The int num contains a value %d.\n", num);
    printf("The pointer ptr refers to a memory address containing the value %d.\n", *ptr);
    
    printf("Changing the value referenced by ptr...\n");
    *ptr = 8;

    printf("The int num contains a value %d\n.", num);
    printf("The pointer ptr refers to a memory address containing the value %d\n.", *ptr);

    return (EXIT_SUCCESS);
}
$ gcc main.c -o pointer
$ ./pointer
The int num contains a value 7.
The pointer ptr refers to a memory address containing the value 7.
Changing the value referenced by ptr...
The int num contains a value 8.
The pointer ptr refers to a memory address containing the value 8.

Note a key concept demonstrated in the above example - changing of the value contained at a particular memory address changes that value for any and all variables pointing to that address. Hence, why reassignment of the value referenced by ptr to 8 simultaneously changes that value for num - they both refer to the same memory address. The above example updates our memory visual like so:

Address 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
Variable ptr
Value 0x20
Address 0x20 0x21 0x22 0x23
Variable num
Value 8

Finally, allow me to give another friendly reminder that although I’ve focused my examples on integer types, pointers can refer to any type!

char ch = 'c';
char *ptr = &c;

float ft = 15.5;
float *ptr = &ft;

// etc.

POINTERS - WHEN

Cool, so pointers are 8-byte [on 64-bit machine] blocks of memory that reference other memory addresses, and they enable us to pull nifty tricks like changing the value contained in a separate variable. Yet, how are they useful? Couldn’t we have just used num directly to change its value from 7 to 8?

You are correct. At face value, the examples above do not reveal any particular usefulness of pointers beyond pulling fun parlor tricks. Yet, these examples do not do justice to the true value of pointers.

Take a look at the following functions.

$ cat main.c
#include <stdio.h>
#include <stdlib.h>

void increment(int num)
{
    num = num + 1;
}

int main(void)
{
    int num = 5;

    printf("Before increment: %d\n", num);
    increment(num);
    printf("After increment %d\n", num);

    return (EXIT_SUCCESS);
}

Here, in the main function, we define an integer num initialized to 5. We print its value, then pass it into a function increment. The increment function receives num and increases its value by 1. Then, back in the main function, we print the value of the variable again. What do you expect to happen?

$ cat main.c
#include <stdio.h>
#include <stdlib.h>

void increment(int num)
{
    num = num + 1;
}

int main(void)
{
    int num = 5;

    printf("Before increment: %d\n", num);
    increment(num);
    printf("After increment %d\n", num);

    return (EXIT_SUCCESS);
}
$ gcc main.c -o increment
$ ./increment
Before increment: 5
After increment: 5
                        

Wait, huh? The value stayed the same!

This funky example demonstrates a fundamental concept of C - function parameters are passed by value. This means that parameters passed to a function in C are copied, and any work done on those variables within the function call are visible only within the scope of that function.

To visualize, when the main function runs, it allocates its own, individual stack of memory to store variables - here, the integer num.

main
int num 5

When the main function calls increment, it passes the value of num. The value of num is copied, and stored in a separate stack only visible within the scope of increment.

main
int num 5
increment
int num 5

Keep in mind - although the variable names are identical, the num contained in main’s stack is completely separate from that in increment’s. It is a copy. Thus why, when increment increases the value of num by 1, the change only registers on the copy of num contained within its scope.

main
int num 5
increment
int num 6

So, we’re programmers, right? Everything is possible, right? How do we get around this?

Allow me to re-introduce your new best friend, pointers.

Since pointers are memory addresses, we can use them to give functions direct access to the memory allocated by particular variables we wish to alter. Let’s update our functions to use pointers:

$ cat main.c
#include <stdio.h>
#include <stdlib.h>

void increment(int *num)
{
    *num = *num + 1;
}

int main(void)
{
    int num = 5;
    int *ptr = #

    printf("Before increment: %d\n", num);
    increment(ptr);
    printf("After increment %d\n", num);

    return (EXIT_SUCCESS);
}

Using pointers, we pass increment the actual memory address referenced by num.

main
int num 5 (address 0x15)
int *ptr address 0x15
increment
int *num address 0x15

Now, using the dereferencing character, we can permanently change the actual value of num, by altering the value located at its memory address.

main
int num 6 (address 0x15)
int *ptr address 0x15
increment
int *num address 0x15

When we run this, everything will work as expected.

$ cat main.c
#include <stdio.h>
#include <stdlib.h>

void increment(int *num)
{
    *num = *num + 1;
}

int main(void)
{
    int num = 5;
    int *ptr = #

    printf("Before increment: %d\n", num);
    increment(ptr);
    printf("After increment %d\n", num);

    return (EXIT_SUCCESS);
}
$ gcc main.c -o increment
$ ./increment
Before increment: 5
After increment: 6

Now, I don’t know about you, but not only does this make pointers unscary to me, but it makes me want to go hang out and get an ice cream with them 😁🍦.

TL;DR

  • Pointers are not scary.
  • Pointers are blocks of memory (8 bytes on 64-bit machines) that reference memory addresses of any data type in C.
  • Pointers are declared using the * character.
  • Memory addresses of variables can be assigned to pointers using the & character.
  • Values stored at the memory addresses pointers refer to can be accessed and changed using the dereferencing * character.
  • Function parameters in C are passed by value (ie. copied); pointers allow us to modify the values of variables across functions.
  • Pointers are your friend.
Written by:

Brennan Baraban, Cohort 7 (SF Campus)