Understanding Pointers in C

Are you currently learning C, but either haven’t learned pointers yet, or do they seem to be totally mysterious? Then this article is totally for you.

Pointers have always been the first point where early programmers hit a really difficult subject, so if you feel stuck with it don’t worry – they are not actually as confusing as they seem, they are just explained using some very confusing terminology.

True, pointers are new type of abstraction (and if you haven’t found out yet software development is mostly about creating, adapting and choosing the right abstractions), but the most confusing part is their name: there are not little arrows inside a computer which you can set to point to anything, of course there isn’t. However if you think of what you are learning as special arrows, everything works — at least initially. It is only later (and probably why you started searching for this article) that the abstraction breaks down. If the pointers point to something and it moves, the pointers now point to something else. You can’t meaningfully increment an arrow, intuitively you expect the increment to go to whatever value the arrow is supposed to point to, but it just doesn’t work that way.

So the first step to understanding pointers in C is to forget the name pointer. Instead we will be using the word address; a variable will not be a ‘pointer to’ anything, it will ‘contain the address of’ something.

Talking about address is useful because, from the point of view of a programmer at least, everything in the computer is located on some address in memory and adresses in a computer works the same way they do in real life, with a couple of exceptions:

So in C we can create a variable of any type, as follows:

1

type name;

for example here is a declaration for a variable with an int value:

1

int age = 1;

however we can define a pointer of any type, and it will be able to contain the address of one variable of that type.

For example here we declare a pointer to an int variable:

1

int *age_ref = null;

Where null is a special marker value that will never ever be the adress of anything. This is super useful because that way we can check if the variable has the actual address of anything (if it does the value of that variable will be something that is not null).

The first thing we are going to do with a pointer is to assign it the adress of something (other people would say making it point to). In C this is done with the & operator – we can get the adress where the age variable is stored and assign it to age_ref by writing:

1

age_ref = &age;

Now it is important to say what did not happen – we did not copy the value of age and we did not change the value of age. We stored the adress of age in age_ref, nothing more.

To go back to our street analog what I just did was give you the address of a house. If I give somebody else the address too and then they burn the house down, if you go there the house you see will also be burned down.

Okay but how is it useful that we have the address for the age variable? Well for one thing, remember that when you call a function in C, the arguments gets passed as a copy? That means that we can’t make a function that, say, reads an int value from a file, but also returns the next position — if we call the function like this:

1

2

3

int value;

int position;

value = read_int_and_get_position(file, position);

Then whatever read_int_and_get_position write to position will be written to its own copy and, like all the variables declared inside a function, be lost when the function returns.

However if we instead passed it along like this:

1

2

3

4

int value;

int position;

int * position_ref = &position;

value = read_int_and_get_position(file, position_ref);

then read_int_and_get_position would get a copy of the address of our position variable, and it could then write to that address. Since the location the address refers to is still the same when it is copied, this works (this is really common pattern that you will see many times in C code).

The real-world analogy gets a bit strained, but imagine that instead of giving you a copy of my house to paint (and then throw away), I took out my address book and wrote my address down on a piece of paper, which I then gave you. You can now drive out to the house, paint it, and go on with your day; as soon as you are done you can tear up the paper and throw it out. I will still have my copy of the house address.

Meanwhile I write down the address on my house to a taxi driver (giving him a copy of the address of the house) and tell him to take me there. The fact that you tore up your copy of the address doesn’t mean anything to the drivers copy (nor to the one in my address book) but the fact that you painted the house at that address means that when I arrive I realize I have to live in a yellow and pink striped house.

But how do we actually get from having the address of the variable to having the value of the variable?

Like this:

1

int d = *d_ref;

crucially if we then write:

1

*d_ref = 4;

The value of the variable whose address is stored in d_ref becomes 4. If, however, we instead wrote this:

1

d_ref = 4;

We would store an address of 4 in d_ref, which would instantly crash the program if the value at that address was ever read from, except for the very unlikely circumstance where there were actually something at that address (and in such a case the program would almost certainly still crash if we changed the value at all).

If we assume a couple helper functions, we can write the read_int_and_get_position function like so:

1

2

3

4

5

6

7

int read_int_and_get_position(file f, int * position_ref) {

    int start_value = get_file_position(f);

    int result = read_next_value(f);

    int end_value = get_file_position(f);

    *position_ref = end_value - start_value;

    return result;

}

You may have seen arrays in C declared like so: char *s. This is because in C arrays and pointers are the same thing, or more correctly, C doesn’t really have any arrays and so pointers have been sorta drafted as a replacement. Worse, C doesn’t have a string type per see, instead strings in C are represented as arrays of char, which means that a string is represented as a pointer to a char.

So how do you know if you have been given the adress of an object or an array, and if it is an array, how many elements does it contain?

There is no general way, you need to read the documentation for the code you are calling! Strings are almost always represented as the address for the char that represents the first letter, then the next letter will be at the next address and so on. To mark the end of the string the value on the address next to the last letter will be null, the special marker address that will saw earlier.

That has three consequences:
A C string cannot contain the value null.
The longer a string is, the more time it takes to get the length.
* To copy a string, you need to iterate over it twice, unless you already
know the string size.

We already know enough to write a function to find the size of the string:

1

2

3

4

5

6

7

8

int strlen(char *s) {

    int count = 0;

    while((*s) != null){

        count++;

        s++;

    }

    return count;

}

A couple things are worth noting:
s is declared as char * and, as such, is expected to contain the address of some char array, which is the same as the address of the first letter in the string.
we can get the next address by doing pointer arithmetic, which is a fancy word for adding or subtracting with pointers. Unlike numbers we can’t multiply addresses and we can only add or subtract numbers, not other addresses (though we can add and subtract variables, so long as they are numbers). This is because adding N to an address should be understood as operating on the element N positions further along, and since elements may be put in memory at different locations between program executions, it makes no sense to do calculations with anything but relative addresses.
we have to compare the value of each element in the array, not the address, hence the dereference operator (the * in the source code). the standard library that ships with every C compiler already includes an strlen function, far more optimised than my naive version. In other words, don’t use this one.

If all you could do with an address type was to save the location of variables you already had they would be of very limited use. Instead imagine a function called house_new which takes no arguments and returns the address of a house (C, of course, has no house type build in, but you can construct one by typedefining a struct). Such a function might have a declaration like so:

1

house * house_new();

in the real world this method is the construction crew: they build our house and then return a copy of the house address so that we know where the house is — if we cared that the house was placed in a particular place we could instead make it so that we handed them the address of an amount of space large enough to hold the house.

Now that the house is built it needs cleaning and painting. Fortunately we have two methods to help us: house_clean and house_paint. Unlike with the house_new method we don’t want to clean and paint just any house, and neither function is able to actually construct a house, so we hand them the address of the house and let them go (dereference) the address themselves – since that way, like the construction crew, any changes they make to that house will be the changes I see when I show up.

If you have guessed that functions in C are allowed to access any memory position, you are right! Mostly anyway, the operation system will kill it if it attempts to access memory outside of its process (to strain the house analogy further we could consider that memory area a foreign country, with an extremely effective UN taking the role of the OS), but anything inside is fair game.

And yes this is bound to lead to bugs, so always be extra careful when you write code with pointers.

One final thing you may see with pointers are libraries that use opaque structures. These are structures which you can only get a pointer to. To actually do anything with them, you have to call methods in the library, parsing that struct along as an argument. The advantages of this is that the library authors are free to change how the struct works internally, and how much size it takes up, while remaining binary compatible with programs that use earlier versions of that library (binary compatible means that you don’t even have to recompile the program that use the library to work with the new version).