Ever wondered what those mysterious * and & symbols mean and how they’re used? Wonder no more.
WHY DO THIS?
- When reading someone else’s code, you’ll never need to worry about encountering pointers.
- Your linked lists will be ridiculously efficient.
Like a metaphorical bridge over the river Styx, pointers are a bridge between the old world of programming and the modern era of languages. Using them helps the programmer feel closer to the bare metal of the machine, but many modern languages, quite rightly, eschew their liberal unregulated freedom because pointers quickly become unwieldy, difficult to follow and dangerous. And it’s true that a good programmer shouldn’t ever need to use pointers, regardless of the language they’re using. But pointers are fascinating, and more importantly, widely misunderstood.
Learning about how pointers work will give you some insight into how variables work as well as how much real work your compiler and your computer are doing on your behalf, turning variables into executable code. If you’ve worked through Mike’s assembler tutorials (see page 106), pointers will also bridge the gap between what you’ve learnt about referencing memory locations and what eventually become variables in most other languages. We’re mentioning variables a lot because a pointer is very similar to a variable(the ‘i’ in ‘i = 1’, for instance), and a pointer can be used to perform very similar operations. But pointers are also far more flexible. This flexibility doesn’t come about because they’re more advanced than variables – and this is key to understanding what pointers are – they’re more powerful because they’re less advanced and less defined in their roles.
The left-hand column lists the various ways of referencing the memory location of the variable (top) and the value it contains (bottom).
By ‘less advanced’ we mean they’re halfway between being what we’d describe as assembler and what we’d describe as a higher function that behaves like a regular variable. It’s this half-way point that’s so important because the programmer can access this entry point and use it to their advantage. This is the reason why they’re so beloved by a certain calibre of C and C++ programmers, which is where you’ll most commonly find pointers in action.
Here’s an example written in the lingua franca of pointers, C++:
int main ()
int variable = 123;
int * new-LV015 104 Coding Ninja.html pointer;
std::cout << “Variable: “ << variable << “n”;
pointer = &variable;
std::cout << “Pointer: “ << *pointer << “n”;
Our example is generic C++ code. Even if you’ve never messed with this language before it should be relatively easy to follow because many languages use a similar syntax. Learning a little C or C++ (the object-oriented augmentation to the original specification) is handy, as it’s what was typically used to build many of the early Linux utilities and shells, and C is used by Linux kernel developers.
The only bits in the above code that may cause confusion are std::cout and <<. The first is the simple function cout for sending text to your standard output. The std:: prefix means that the function is coming from the namespace/class called std, which we imported from the iostream library in the very first line. The double less-than symbols, ‘<<’, are used here just as they are in Bash on the Linux command line, and redirect the data to the standard input.
We’ve saved this to a file called pointer.cpp. If you’ve got any kind of build environment installed, and that includes those times you’ve allowed your package manager to build things from source, you’ll be able to compile and link this file into an executable binary by typing:
g++ -Wall pointer.cpp -o pointer
After a few moment, the build process will finish and you’ll find that an executable file called pointer has been created in the same folder. You can run this as you would any local executable by typing ./pointer. The output should look like the following:
Our source code first creates an integer variable called variable, and use this to store the number value 123. We then create another integer variable and call this one pointer. You should also have noticed that between the int and the name, there’s an asterisk (*) and this is where pointers enter the scene, centre stage. The asterisk is one half of the unholy character union that signals the use of pointers, the other character being & (ampersand). The asterisk comes first because it means that we’re creating a pointer, rather than a fully fledged variable. A pointer doesn’t store the value, as with the int variable = 123 statement. Instead, it holds a reference to a variable defined by the type that comes before the asterisk. We’ve created an object that will hold a reference to a variable that’s going to be an integer. This reference is usually going to be the memory location of where a variable is being held, but the end result is always that it returns the value references by the memory location, rather than returning the memory location itself.
Sometimes the exact position of the asterisk will change, but it’s always used to signify the use of a pointer. It’s what’s known as a unary operator, which means it only operates on a single operand – the value that follows it. Both the symbols used to work with pointers are unary operators. You could, for example, forget about the ambiguity of int and * and just tell yourself the int * string of characters is a special type that denotes a pointer to a variable that holds an integer, but because * is an operator rather than a real type definition, this would be misleading. You need to keep using the asterisk symbol whenever you reference an object you’re using as a pointer because it’s not a specific data type, it’s just a way of passing a reference. This is important.
On the following line, we simply output the value of the integer we created to illustrate that everything is working as expected. It’s the line following this that shows pointers in action.
pointer = &variable;
Here’s the other half of the character union, the ampersand symbol (&). This is a unary operator that’s used to return the memory location/address of a variable. Yes, the real memory location that’s currently holding the value of ‘variable’. This is the kind of thing you’d expect to be doing with assembler rather than a modern programming environment, but the ability to do this has survived because it enables you to perform a few neat tricks that are difficult to pull off as efficiently with any other method. With the address of variable passed to pointer, and with the definition of pointer as a reference to an integer, the compiler has everything it needs to return the value being held by variable, which is what we do on the last line.
Knowing the size of the value being held at the location being referenced is vital for the compiler to know how much data to return and how it should be interpreted. This is why the output from *pointer is the value being referenced by the memory location being held in the pointer and not actually memory location itself. Which in turn is why the value that’s output is the value being held by variable and not anything else. If you wanted to see the actual memory location value, just remove the * and rebuild the code. The output from that line will show something like ‘Pointer: 0x7ffcba364334’, which is the real memory location of ‘variable’ being held by the pointer.
There’s another usage of pointers as a dereference operator. This is where you’d assign the value referenced by a pointer to another variable, like this:
int newvariable = *pointer;
The value now held in newvariable is a copy of the value referenced by ‘pointer’ and not a pointer, if that makes sense. If the pointer value changes, newvariable won’t change because it’s been decoupled/dereferenced from the pointer. And that’s all there is to pointers – the creation of a variable used for a reference and the use of the & operator to return the memory address of where something is being stored. Because it’s a reference to a memory location, if the value being held at that location changes then so to will the value returned by the pointer. Pointers are useful when you don’t want to copy or duplicate large data types – you can use them to pass functions to other functions, for example, and they’re used to create linked lists.
Programming languages that support pointers aren’t always able to report on errors that might be generated by improper use, so you need to be careful.
There’s one important side effect: you need to be careful that you don’t leave any loose ends or broken pointers. This is collectively known as garbage collection, and C and C++ in particular do very little to help the programmer. You need to make sure you free up memory and unused pointers yourself. If you want to play with pointers, we’d recommend a modern language with pointer support and automatic garbage collection, such as Go. Its implementation of pointers is very similar to C and C++, which helps with experimentation.
Pointers are an anachronism that are probably best avoided other than in specific circumstances. In C and C++ they’re the only way to do certain things with complex data types, and because they’re so primitive, they’re lightning fast. But knowing how they work and what they’re capable of is still a useful exercise. They crop up when reading lots of Linux code, especially in the kernel, and they’re another useful technique when a programming language doesn’t seem to offer something similar itself.