C++: Pointers and References
In this tutorial we're going to talk about two core concepts of C++: references and pointers. Don't worry if you get a little bit confused by different concepts, this is the area in which people usually trip up. In fact, I would say this is where the largest proportion of people learning C++ just give up. Do not give up. If you don't understand something then try reading it again, and if you're still having problems then try and seek out some help to fill the gap(s) in your knowledge.
References
Firstly, let's talk about references because they're the simpler of the two. References are essentially just aliases (other names) for things, and so as the name suggests they essentially just reference other things. If we create an integer variable and then create a reference which refers to this, any mention of the reference will be exactly the same as a mention to original variable. Given the fact that references are created much like normal variables, with the addition of an ampersand (&
) before the reference name, the purpose of the following code should be reasonably obvious:
1 2 3 4 |
|
In the above we create an integer, 'x', and then we create a reference, 'y', which is a reference to an integer data-type, and tell it to refer to 'x' (and we then output both 'x' and 'y'). After the first two lines, any usage of 'y' is exactly the same as usage of 'x', as demonstrated by the cout
. It is said that the second line "binds the reference 'y' to 'x'". Similarly, references work just great with class objects if the correct data-type is used:
1 2 3 4 5 6 7 |
|
So now you should know some basics of how references actually work (above, 'x' => 'obj' and 'y' => 'obj.a'), we should probably talk about the advantages and limitations of them. The first important point is that a reference must be bound (the value it refers to must be defined) when the reference is defined. The following code, for example, makes no sense:
1 2 |
|
Above we want define that 'y' is another name for 'x' in a later portion of code, however there is now no way for us to state what 'y' is a name for. We can't say y = x
because that would mean "assign 'x' to whatever is referred to by 'y'", and 'y' doesn't refer to anything!
The second important restriction of references is that we cannot reference something that we can't assign to. The following, for example, fails:
1
|
|
References just don't work in a way that would allow for the behaviour above, and we can predict this as we cannot assign a value to '5' (5 = 9
, for example, makes absolutely no sense).
Thirdly, the type of reference usually has to be the same as the referred-to object. We can't do the following:
1 2 |
|
Even though we can convert an int
to a short
in usual usage, what we would be saying in the above is that "'y' is another name for a 'short' called 'x'" -- 'x', however, is not a short
, and hence that code will not compile. There are some exceptions to this rule with regards to base and derived classes, but we won't talk about that right now as it should make sense once you run into it and probably won't make a whole lot of sense to you right now.
So with the main restrictions out of the way, a vital question is coming into view... why/where are references actually useful? Why don't we just use the original name? The answer is actually pretty simple and is all to do with scope.
If a variable is defined in the main
function and we want to play with it in another function, at the moment this is an impossible task for us as the variable is specific (local) to the function where it was created. The closest we could really get is passing variables in as parameters (which creates copies of the variables), playing with these, and then returning these values. With references, however, we don't need to mess around with all this copying! Our hypothetical function here could take references to the local variables as parameters, and hence play with the variables directly via the references rather than using copies! This makes the function way more powerful and also makes it far more memory efficient (not making those copies is great for memory usage!). A good example of using references in this way might be a function which switches the values of two integers:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
With the above, the values of the 'x' and 'y' variables in 'main' would be switched without any further messing about! Note, importantly, that we do not define 'temp' as a reference -- there is a distinct difference between 'temp' having the value of 'a', and 'temp' being the same thing as 'a'. If 'temp' was an integer reference in this case, both variables would be set to the value of the second parameter (run the code through in your head or in a compiler and you should see why).
So now that references should make some sense (try making a function much like our swap
function which makes use of references, perhaps for something more useful, and you should be pretty happy with using them), let's move on to the bigger brothers of references... pointers.
Pointers
Take a look at the following function which makes use of references:
1 2 3 4 5 |
|
This may seem fine on the surface, but what would happen if we passed the same variable in for both parameters? The answer is that we'd probably get a result which was not intended. If we pass in a variable, 'x', for both parameters, which is equal to one, instead of a result of 'x = 3' (1+1+1) which might be expected, we'll get a result of 'x = 4' (1+1+2) as the first line of the function modifies the value 'b', unintentionally, which is then added to 'a' (if you're not quite sure how this works, just step through the code in your head as if you are the compiler).
One 'solution' to the problem demonstrated above is to check if the two parameters refer to the same thing or not. Luckily for us, 'regular' variables in C++ are all identified in some unique way, and this unique identifier can be found by using the ampersand (&
) operator before a variable (which is, importantly, entirely different to the ampersand symbol used in a reference declaration). Hence if the two parameters in our function are equal, &a == &b
will be true, and with this we can output a warning:
1 2 3 4 5 6 7 |
|
So that solves our specific problem there, but if &x
truly is a unique identifier, surely there should be a way of getting back to our original variable (i.e. x
) from it? It turns out, there is. We can use the asterisk (*
) operator, named the dereference operator (a cryptic name, I know), to go back from this unique value to the variable itself, hence the notation *&x;
simply refers to "the value represented by the identifier of the variable 'x'", so just x
itself.
In reality, the &
operator doesn't just represent some arbitrary identifier, it actually represents a location in memory. If you aren't aware, variables are all just areas of memory (RAM) which we've given names to -- internally, computers use numbers to specify where exactly in memory something is, and these locations are called memory addresses. These addresses are usually referred to in hexadecimal, and as can can be seen by simply cout
ing the memory address of a value (i.e. &x
assuming 'x' exists), are usually in a form like "0x5A5A5A". With this in mind, the &
operator is sometimes referred to as the "address of" operator as it finds the memory address of something, and the *
operator is sometimes referred to as the "value at" operator as it finds the value which is at a certain address. All named variables have addresses and references are essentially just addresses which are equal to the address of the referred-to object -- keep in mind, however, that temporary things such as the return values of functions do not have addresses.
"This is all very well", I hear you cry, "but I thought we were supposed to be learning about pointers!" -- in fact, pointers are essentially just what we've been talking about. Not only can we dereference a memory address such as &x
(assuming the variable 'x' exists), but we can also store it, and we do so through things named pointers. Quite simply, a pointer is just a stored memory address with associated data-type, however they are much more powerful than you may initially imagine.
A pointer is defined by using an asterisk just before the name of a 'regular' variable, so we could create a basic pointer to an integer like so:
1 2 |
|
Like references, pointer data-types should also represent what they point to in the majority of cases. Anyway, from there in the code, we can do all the things we could do to &x
to 'p', since the value 'p' is actually just a memory address (as shown in the initialisation) -- so *p
("the value at the address 'p'") is the same thing as *&x
, which is the same thing as x
. So we could re-create our 'swap' function using pointers if we wanted to, with something like the following:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We can even make pointers to pointers if we want to since pointers actually occupy a space in memory, so something like the following (which looks far scarier than it is) will work fine:
1 2 3 |
|
In the above, **p2
("the value of the value at the address p2") is the same thing as 'x'. This can be a little mind-bending, so don't worry too much if you're getting confused over this kind of pointer nesting, but my aim here was simply to let you know that this kind of functionality is possible.
Since pointers, unlike references, can change what they point at, another interesting question is: "So what if we want to point to nothing?". Well it turns out there's a special value that we can assign any pointer to which will make it essentially not point to anywhere -- this value is named NULL
and it can be convenient for initialising pointers:
1
|
|
Note that unlike references, pointers don't actually need to be initialised at declaration, however it's usually good practise to set pointers which don't have an address value to NULL. In the example above, at any point in the future we can simply check if this integer pointer, 'ptr', is pointing to anything by checking if it (which otherwise would be a memory address) is NULL (ptr == NULL
=> 'true'). The more curious among you may also be wondering what happens if we try to dereference a NULL pointer (e.g. int* p = NULL; cout<< *p << endl;
), and the most correct answer to this is really: "we don't know". The C++ language doesn't define the result in this case, and so absolutely anything could happen and you can't really complain -- in practise, this will usually lead to a program crash.
Although you should be able to understand both pointers and references at this point in the tutorial - being able to point out the differences between the two - the advantages and disadvantages of using one over the other may be slightly unclear to you. Simply put, if you can use references, you probably should, otherwise you should use pointers. Basically, it turns out that being able to change the address value at any point and actually being a "real thing" in memory is super useful, and so pointers are by far the more powerful of the two -- we can have pointers as member variables in classes and all sorts of things which just make them insanely useful. We cover more complex uses of pointers later in the course -- one of the most useful uses of pointers is dynamic memory allocation which we'll cover in detail at some point -- but for now you may just have to take my word for it that pointers are really powerful.