10 Days of Basic Programming, Day 7: Pointers
If you have been closely following these series of articles pay close attention to today’s lesson as pointers are usually the hardest to grasp.
So what’s a pointer? You may ask. Well the name certainly gives away what it may be: it’s something that points to something.
The simplest way to put it: a pointer is a number that represents a concrete area of RAM housing certain data. In other words, a pointer is like an address, it tells you where someone lives so you can go visit him.
When we talk about RAM we have to talk about direct memory access.
With devices such as a hard drive, accessing certain data in a certain area of the hard drive requires the hard drive to move, or seek, into that area first, then read or write the contents. RAM on the other hand has no seeking, data is accessed directly using a number that tells it where to access the data at. This number is of course the pointer.
It’s called a pointer because it is indeed pointing at a very specific part of RAM. Kinda like this:
The arrow would be the pointer, it is nothing but a number that represents an address in RAM. With a pointer we can directly read or write from RAM, but there is a catch: we need to know how much data we are reading or writing. In terms of bytes, we could be reading just one byte, or we could be reading 8 bytes.
A CPU allows you to read in just one instruction as many bytes as its registers and bus allow it. A 32 bit machine can read/write up to 4 bytes at a time, a 64 bit machine can read/write up to 8 bytes. Or we can read less than that, we can just read/write 2 bytes or even just 1.
Now remember what defined the size of a data? Yes, it’s type.
Before we used to declare a variable like this:
type name = value
We used that to, for example, declare integers:
int my_var = 0
It is obvious we are going to do something similar with pointers: we need to declare the type of the variable the pointer is pointing to.
A pointer is declared by using the variable type followed by the special character “*”, for example:
The above code declares the existence of a pointer called “my_pointer”, and the data it is pointing to should be treated as an int.
Now it is extremely important for you to know that the above allocates space for the pointer and not for the variable it is pointing to, and making any changes to “my_pointer” results in the pointer being changed, not the variable it is pointing to, for example:
int* my_pointer = 0
The above code does not initialize the integer variable to 0, it initializes the pointer to 0. To actually access the variable being pointed to we have to use what is called pointer dereference, which tells the compiler that you are accessing the data and not the pointer, dereferencing a pointer is done with the asterisk symbol (the *), like this:
*my_pointer = 0
int my_var = *my_pointer
The asterisk tells the compiler you are accessing the data associated to the pointer, not the pointer itself.
Another important thing to know is that you can have pointers to pointers, no matter how many nests of pointers you have, for example:
The above declares a pointer to a pointer to an int. The only thing you have to know is that doing *my_pointer will access the pointer to the int, not the int, you’d have to do two dereferences, the first one to access the second pointer and the second one to access the int: **my_pointer.
Now if you’ve been paying attention you’d know that I said that declaring a pointer only allocates space for the pointer itself, not for the data associated with the pointer. Back when we used simple variables, if we declared an int inside the global scope the compiler would allocate space for that int in the binary’s data segment (all binaries have a data segment, where global data goes, and a code segment), or if we declared an int in a local scope then the compiler will allocate space for it in the stack. But with pointer we are only allocating data for the pointer itself, be it on the global data segment or on the stack, but the variable associated to the pointer is not allocated, in fact, trying to dereference a pointer that is not initialized to an allocated data will result in an access violation, which will cause the program to crash.
So how do I allocate space for the variable? Well there’s three places where variables could be allocated: the global data segment, the stack and the heap. So far we’ve seen the stack and data segment, but the heap?
The heap is a big chunk of RAM that the operating system assigns for your program to be able to allocate memory in a dynamic way.
Being able to allocate on stack or data segment has one obvious problem: it is a static way of allocating memory, it will always allocate the same space. But anyone knows that there’s a lot of things that would make the program have to allocate a variable amount of RAM. For example a program that reads files stored in the hard drive can never know how many files there will be and it has to allocate space for each file, so it’s obvious it will not always be allocating the same memory but rather depending on its environment.
This is where the heap comes into place: it allows you to allocate a variable amount of data, and when you no longer need it, you can free the allocated space to be used for other programs or other parts of your code.
In the C language there are two important functions that do this: malloc() and free().
Malloc takes in an integer representing the amount of bytes you want to allocate and returns a pointer to the chunk of RAM allocated. Free takes in a pointer and marks the allocated area as free and available to use by other code.
Here’s an example of using malloc:
void* ptr = malloc(10);
The above C code allocates 10 bytes and stores it in the pointer called “ptr”, notice how the pointer is a pointer to void, this is what malloc actually returns. A pointer to void is, in general terms, it’s a number that we know it’s a pointer, but we don’t know what it points to.
And this is exactly what happens, malloc can be used to allocate any amount of bytes, so it can’t possibly know what the interpretation of that data is, so it just returns a pointer to void. Of course there is one problem with void pointers, the C compiler won’t let you use them since it doesn’t know how. So we need to cast void pointers into normal pointers depending on what we wanna use with it, in this example I want to allocate space for an int:
int* ptr = (int*)malloc(sizeof(int));
We don’t need to allocate space for just one int, we can also allocate space for a bunch of ints, essentially being able to create a dynamic array.
int* ptr = (int*)malloc(sizeof(int)*10);
In the above code I allocate space enough to hold 10 integers, and I can use ptr the same way as an array: ptr, ptr, ptr, …, ptr.
Now it is extremely important to know that we have to free dynamically allocated resources when they are no longer being used, otherwise we’d be eating up RAM and our programs will eventually crash.
On higher level languages, allocation and deallocation of RAM is handled automatically by the language, usually through means of a garbage collector. They also eliminate the need to care about pointers as they are also handled, for example in Java and Python, instances to objects are all pointers, but you don’t handle them yourself, the language does. On C++ on the other hand, you can have objects on stack. Next month we’ll be learning what an object is.
Now you may noticed that I have talked about there being three distinct areas of RAM for any program: global data, stack and heap. And I have shown you how to use all of them and how to use pointers to heap, but some of you may ask, isn’t stack and global data still in RAM? Shouldn’t it be possible to obtain the memory address, that is pointer, of a variable residing in stack or global space?
Yes, indeed stack and global space is still just another part of RAM, and like I said, a pointer is the numeric representation of an address in RAM so it is indeed possible to obtain the pointer to a variable in stack or global space. In C this is done with the ampersand operator (&), and here’s an example:
int x = 0;
int* ptr = &x;
The variable ptr will now contain the address in RAM of the variable x.
Now this is only doable if the variable resides in RAM, but in C you can declare a variable as residing in a CPU register:
register int x = 0;
This is normally done to speed up programs, as accessing a CPU register is much faster than accessing RAM, however CPU registers reside inside the CPU, not RAM, so they don’t really have a memory address.
Other than that special case of variables residing in CPU registers, you can obtain the memory address of pretty much anything, even of pointers:
int* x = malloc(sizeof(int));
int** ptr = %x;
And of course, structures:
struct MyStruct my_struct;
struct MyStruct* my_ptr = &my_struct;
The only difference here is that we use the arrow operator (->) to access data inside a pointer to struct instead of using a dot (.), like this:
Well that’s about it for now, I’m gonna try to finish these series in the following days and start with 10 Days of Intermediate Programming.