C pointers: all you must know

João Loss
13 min readJan 23, 2024

--

I’m 100% sure that by the end the of this read your C codes will get to the next level, just trust me and keep reading!

Let’s get started!

Topics:

  1. Why should I learn it?
  2. Memory and variables
  3. What’s a pointer?
  4. Syntax
  5. More details
  6. Pointers and functions
  7. Pointers and arrays
  8. One step further

Why should I learn it?

There are a lot of reasons to learn and use pointers in C programs, but I’ll list just three of them:

  1. Makes it possible to simulate “pass by reference” functions
  2. Makes it possible to efficiently interact with complex data structures
  3. Learning this tool is the first step to write high-performance C programs in terms of memory allocation and speed

Memory and variables

Before we talk about pointers it’s crucial to have clear in mind the idea and concept of variables. A correct understanding of it will be extremely helpful later.

So what’s a variable for a computer?

int number = 10;
char letter = 'a';

In the above block of code we’ve declared two variables: an int variable called “number” with an initial value of 10 and a char variable called “letter” with an initial value of ‘a’. But what is really going on in the computer RAM?

It stands out that “number” and “letter” are just names that make our lives much easier. What the computer is actually doing is separating for us two spaces in RAM (one space of 4 bytes (for the int variable) and another space of 1 byte (for the char variable) — obs.: this is one of the reasons of specifying the type of a variable: to tell the computer the amount of memory we need) and “nicknaming” these spaces according to the name of each variable, simplifying our work when we want to interact with the memory (for example, when we assign a value, as the above code does).

Illustration of a variable in memory

Applying to the example code:

Illustration of ‘number’ and ‘letter’ in memory

In short, a variable is just an abstraction of memory space that allows us to have a friendly interaction with it. Keep this clear in mind!

What’s a pointer?

Now we’ve understood the basics about variables, we can move on and learn about pointers.

A pointer is a powerful tool of C language, and although it may seem a little confusing at first, it is absolutely necessary to code C programs (you can be sure of that)!

So, what is it?

The definition isn’t a rocket science: a pointer, as it suggests, is a variable that points somewhere else! So it’s easy to conclude that the content of a pointer isn’t just a final useful value as we’ve seen in the previous topic, the content of a pointer is an address! The following image illustrates a pointer called “p” (in the next topic we’ll see why to use the asterisk) that points to a an int variable called “x” storing the number 10.

Illustration of a variable and a pointer in memory

And how does a pointer store an address?

Let’s first take a look at the syntax to help us answer the question.

Syntax

So how is a pointer declared? How does the computer know it’s a pointer and not just a common variable?

To inform the computer we are declaring a pointer we need to use an asterisk (*):

int* pointerToInt;
char* pointerToChar;

The above block of code declares two pointers. In the first line it’s declared a pointer to an int variable called “pointerToInt”, then it’s declared a pointer to a char variable called “pointerToChar”. Notice that we haven’t assigned any address to these pointers yet, they were just declared.

But to assign address to a pointer, we first need to know how to get the address of a variable. To do so we use another important symbol: the ampersand (&). When we put it before a variable we take its address instead of its stored value:

// common variables:
int number = 10;
char letter = 'a';

// pointers:
int* pointerToInt;
char* pointerToChar;

// assignment:
pointerToInt = &number;
pointerToChar = &letter;

The above code declares two variables, two pointers and then assign the address of “number” to “pointerToInt” and the address of “letter” to “pointerToChar”.

Now, how can we access the content of the pointed variable?

To do so we will you the asterisk again! So notice that the asterisk has different roles in different contexts! When we use it in the declaration, its role is to inform the computer that this is a pointer; when it’s used in arithmetic operations or in assignments, for example, its role is to make available the content value of the pointed variable!

// common variables:
int number = 10;
char letter = 'a';

// pointers:
int* pointerToInt;
char* pointerToChar;

// address assignment:
pointerToInt = &number;
pointerToChar = &letter;

// assigning the values of the variables pointed by the pointers:
int anotherNumber = *pointerToInt; // same as "anotherNumber = number;"
char anotherLetter = *pointerToChar; // same as "anotherLetter = letter;"

So, what should be the value of “anotherNumber” and “anotherLetter”? Perfect! “anotherNumber” will store 10 and “anotherLetter” will store ‘a’!

We can also modify the content value of a variable using a pointer:

int x = 10;
int* p = &x;

*p = (*p) * 2;

Now, what should be the value of “x” at the end? Great!! Should be 20! Because in the last line we access the content of the variable pointed by “p” and change its value by multiplying it by 2!

To practice:

int x = 5, y = 100;
int *p1 = &x, *p2 = &y;
int aux = *p1;

*p1 = *p2;
*p2 = aux;

// what should the values of "x" and "y" be at the end?
// run it and check if you were right!

To have a deeper learning about how a pointer actually works, let’s look at some low-level details.

More details

There are some low-level information about how pointers work that will make somethings much more clear. Just focus and keep reading, I’m sure it will be helpful!

First of all, it’s important to know that the memory where our variables are stored is organized in blocks of bytes, and each block can store 1 byte (so, if we store an int variable it will use 4 memory blocks).

Obs.: these memory and storage values may change depending on the compiler and the system.

int iii = 255
short sss = -1;

If we look at these two variables in memory, it would look something like this:

Font (obs.: numbers are in hexadecimal notation)

The image shows an int variable called “iii” occupying 4 blocks (4 bytes) with a value of 255 (or FF in hexadecimal) and a short int variable called “sss” occupying 2 blocks (2 bytes) with a value of -1 (or FFFF in hexadecimal).

Obs.: you can search more about binary and hexadecimal notations to make things clearer, I won’t do that here.

Right, but what if we want a pointer to “iii”? What would it be like in memory?

int iii = 255
short sss = -1;

int* ptr = &iii;

In memory the above code would look like this:

Font

Note that the value of “ptr” is just the address number of the first block of “iii”. This’s how a pointer works: all pointers point to the first memory block of a variable or any other data structure! That is the reason of specifying the type of a pointer. The computer needs to know the number of memory blocks used by the pointed variable to manipulate all data correctly, as each type has a specific size (a char data will be in just 1 block, while an int data will need 4 blocks to be stored) and the content value of a pointer itself can only inform where the pointed variable begins in the memory!

In short, the value of a pointer indicates where the data begins and the type of a pointer indicates where it ends!

So you can correctly conclude that all pointers have the same size, because all pointers store just the address of one memory block (the first one)!

Pointers and functions

Now let’s see how pointers can improve our C functions.

If you are familiar with C language and how functions in general work, you will note that C always uses “pass by value” (or “call by value”) to pass arguments to functions, i. e., the original variables passed as parameter can’t be changed, as what happens is that the arguments value are copied to the variables used inside the function. So, if we want to change an outside variable we need to return a value from the function and make an assignment.

Example:

int doubleIt(int n) {
return n*2;
}

int main() {
int num = 5;
doubleIt(num); // "num" won't be changed
num = doubleIt(num); // now it will change
}

In short, we can only change one variable at a time with this method. But we all know that in many cases it would be great if we could return more than one value by modifying the arguments content. And this is exactly one of the things that pointers allow us to do! Using pointers we can provide the address of a variable instead of its value, so that the function can go to the passed address and change its value. i. e., we can simulate a “pass by reference” function!

Example:

void doubleIt(int n, int* result) {
*result = n*2;
}

int main() {
int num = 5;
doubleIt(num, &num); // now, "num" will be 10
}

What happens in above code is that when the function is called the value of “num” is copied to “n” and the address of “num” is copied to the pointer “result”, making it possible to change the content value stored at the address inside the function.

Obs.: now it’s easy to understand the reason of using the ampersand in scanf function, as its main purpose is changing the values of the arguments!

Pointers and arrays

If you’ve really understood what a pointer is and how it works, you will easily get the concept of array in C language, as an array is just a “special” pointer.

So, what kind of pointer is an array?

The main difference is that an array is a pointer to not just one variable, but to a sequence of variables. In practice, instead of pointing to the beginning of a single int (for example), it’ll point to the beginning of a sequence of int. So keep this in mind: in C language an array is nothing more than a pointer!

Let’s see an example:

int nums[3];

The above code is just asking the computer a sequence of 3 blocks of int. And the address of the first block of the first int element will be stored in “nums” (just as we’ve seen in “more details”)! In other words: “nums” is the name of a pointer to the first block of an int element in a sequence of 3 ints.

Illustration of “nums” array

With this idea in mind, we can now talk about arithmetic operations with pointers (actually we can only add and subtract a pointer). But why should we add or subtract a pointer? Well, when we add or subtract an address when can go through memory and access different elements! If we add 2 to “nums”, for example, we will access the third int element:

Illustration of an arithmetic operation with pointers

Actually, if you’ve already worked with arrays in C language, you’ve certainly done arithmetic operations with pointers, you just didn’t know! When we use the brackets to access some element by its index, we are indeed making sum operations!

Let’s understand what is really going on in the following code:

int nums[3];
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;

The code above is just assigning values to the array called “nums” (which is actually a pointer, as we’ve just seen). But how do these assignments work? In fact, the brackets are just an user-friendly way to perform sum operations with pointers! With this in mind, now we can write the same code without the brackets:

int nums[3];
*(nums + 0) = 1; // or just "*(nums) = 1;"
*(nums + 1) = 2;
*(nums + 2) = 3;

So when we use the brackets to access an index of an array we are actually making arithmetic operations! “nums[i]” means: starting from the address stored in “nums” make “i” jump(s) and access the value stored there!

Obs.: how does the computer know how many blocks of memory it needs to jump to access the next element? (i. e.: how does it know the jump size?) Here is another reason to specify the type of a pointer: to tell the computer the “size of a jump” (because the jump will be the same size of the element type)! For an int array, each jump will be 4 blocks in size (as the above image illustrates), but for a char array, each jump will be 1 block in size (remember that these values may change depending on the compiler and the system)!

That said, it’s extremely important to have clear in mind that operations with pointers are different from operations with the content pointed by the pointer!!!

int nums[3];
*(nums + 0) = 1;
*(nums + 1) = 2;
*(nums + 2) = 3;

int* p = nums + 1;
int x = *(nums) + 1;

Here we have both types of operations. On the penultimate line there is an operation with pointers itself, where the pointer “p” will store an address (in this case the address of the second element of the array). And in the last line there is an operation with the content pointed by “nums”, where the variable “x” will store the number 2 (1 (the value of the first element of the array) plus 1 of the operation).

Obs.: if you are confused, try to imagine how it is working in memory by looking at the images above.

To fix:

int nums[3];
*(nums + 0) = 1;
*(nums + 1) = 2;
*(nums + 2) = 3;

int* p1 = nums;
int* p2 = nums + 2;

int x = (*p1) * 2;
int y = (*p2) * 2;

*p1 = y;
*p2 = x;
*(nums + 1) = *(nums + 1) + 1;

printf("%d %d %d\n", nums[0], nums[1], nums[2]);

What values will be printed at the end? Think and run it to check if you got it right!

One step further

Right, but what about pointer to pointers? Is that possible? Is it useful?

Here things start to get more interesting. Pointers to pointer isn’t just possible and useful, it’s more common than you think!

The C language allows you to create pointers with different pointing levels:

int *p; // pointer
int **pp; // pointer to pointer
int ***ppp; // pointer to pointer to pointer
...

// the number of asterisks indicates the number of pointing levels it has

And to access their final content we use the same previous logic:

int *p;
int **pp;
int ***ppp;

// assignments using the final contents pointed by the pointers above
int x = *p;
int y = **pp;
int z = ***ppp;

Okay, but where might we use it?

If you’ve ever solved some more complex problem in C language, you’ve certainly already used matrices! And what is a matrix? A matrix is nothing more than a pointer to pointer!

Let’s see how does it works:

int mat[2][3]; // matrix with 2 lines of 3 columns

So, what is “mat” in fact? It turns out that “mat” is a pointer to a sequence of 2 arrays (the lines), where each array is a sequence of 3 int elements (the columns)! i. e. every matrix is just a pointer to pointer!

Illustration of “mat”

And how does the arithmetic work here? If we have a pointer with “n” pointing levels, we will need “n” levels of arithmetic operations to access a final value! Let’s understand it using the “mat” matrix (a two levels pointer (or two-dimensional), but the logic is the same for “n” levels).

In the first level, if we increment “mat” we will jump 3 int elements in memory (i. e. we will jump an entire int array with 3 element). In the second level, we will jump 1 element per increment.

Let’s see an example using “mat”:

int mat[2][3];

mat[0][2] = 10;
mat[1][1] = 5;

The first assignment access the first array (index 0) than access the third element (index 2). The second assignment access the second array (index 1) than access the second element (index 1). We could do the same thing using arithmetic operations:

int mat[2][3];

*(*(mat + 0) + 2) = 10; // same as "mat[0][2] = 10;"
*(*(mat + 1) + 1) = 5; // same as "mat[1][1] = 5;"

With “*(mat + i)” we are accessing the address of the array (or row) indexed by “i” (remember that “mat” is a pointer to pointer, so “*mat” — or “*(mat + i)” — is still an address — to “arrive” in the final value it would be “**mat”). And with “*(*(mat + i) + j)” we are accessing the value of the element indexed by “j” inside the chosen array.

What if a pointer to pointer to pointer??? You can consider it in whichever way is most appropriate to solve your specific problem! Maybe an array of matrices (where each element of the array is a matrix) or a matrix of arrays (where each position of the matrix is an array)!

Here we start to understand how complex and advanced this can get with pointers! Just be creative!

Thank you for your attention! These were just the first steps with C pointers, keep learning and practicing!

--

--