To create a reference variable, we place an &
character between the type and the identifier. If an integer has a type of int
, a reference to an integer has a type of int&
. For example:
int x { 5 };
int& ReferenceToX { x };
Typically, the &
is placed immediately after the type, as in the above example, but the following are all equivalent:
int& Reference1 { x };
int & Reference2 { x };
int &Reference3 { x };
We can think of references as "aliases" for the original variable. Both the original identifier, and all references, point to the same value stored in memory.
After running the following code, the value of both x
and y
will be 2
:
int x { 1 };
int& y { x };
y++;
The reference can be used interchangeably with the original variable. For example, anywhere an int
is expected, an int&
can be used instead. We can use all the same operators on an int&
that we could do with an int
.
After running this code, what is the value of x
and y
?
int x { 5 };
int& y { x };
x += y;
We are now ready to address the problem we introduced in the previous chapter.
By setting a parameter of a function to be a reference type, we can pass arguments to that function as references ("pass by reference") rather than passing a copy of the object with the same value ("pass by value").
After running the below code, the value of x
will be 2
.
void Increment(int& Number) {
Number++;
}
int x { 1 };
Increment(x);
After running this code, what is the value of x
and y
?
void Increment(int& x, int y) {
x++;
y++;
}
int x { 1 };
int y { 1 };
Increment(x, y);
Lets also go back to our original problem, involving our custom objects. The addition of the &
character to the parameter list of the Combat
function allows us to pass our objects around as references.
This means we can keep a single copy of our objects in our game. Our functions can share the same object without that object needing to be a global variable.
The following code will log out 100
as expected:
#include <iostream>
using namespace std;
class Character {
public:
int Health { 150 };
}
void Combat(Character& Monster) {
Monster.Health -= 50;
}
int Main() {
Character Monster;
Combat(Monster);
cout << Monster.Health;
}
Note, we don't need to specifically create a separate reference value to pass to our function. For example, if a function expects an int&
and we pass it an int
, the conversion will happen automatically.
const
We introduced references here to solve a specific problem around being able to update objects that exist in other scopes - but that is not the only use of references.
Even if a function is not going to change a value that is passed into it, it can still be worth passing that value in as a reference.
Complex objects can take a lot of memory, and the process of copying them can also consume a lot of CPU cycles. As a result, pass-by-reference is often the preferred method of passing non-trivial objects to functions, unless we have a specific need to create a copy.
When a function is going to read the values of a reference, and not change anything, we should add the const
specifier to the parameter:
void LogHealth(const Character& Monster) {
cout << Monster.Health;
}
The const
specifier here asks the compiler to prevent the variable from being modified. We can't modify a const
reference in our function.
We also can't pass the reference into an argument into a nested function call, unless that other function has also indicated that parameter will be treating as a const
reference.
This is particularly useful in larger software projects. When a developer creates a variable and then passes a reference to it off to a function, they're generally going to want to know the implications of that.
If the parameter they are passing the variable into is marked as const
, they can be fairly confident that the function is just going to read the variable, not change it.
If it wasn't marked as const
, that means they may need to be more mindful, and understand what the function is going to do with their data.
After running this code, what is the value of x
?
void Increment(const int& x) {
x++;
}
int x { 1 };
Increment(x);
We talk more about const
and it's benefits on the later chapter on Clean Code. For now, lets lay out the two big limitations of references.
References are, in effect, memory locations. Allowing memory locations to be passed around in our software can introduce a lot of complexity and bugs. These bugs can be simultaneously hard to detect and unpredictable in their effects.
To combat this, references have two major restrictions, that ward off the biggest sources of these bugs.
References must be initialised with a value. That means we can't do this:
// Error! We need to provide `MyReference` with a value
int& MyReference;
References cannot be re-assigned. That means we can't do this:
int x;
int y;
int& MyReference { x };
// Error! We can't reassign a reference
MyReference = y;
For use cases where we need to do one of these things, there is another type of reference that doesn't have these restrictions. These are called pointers, and we'll see them soon.
For now, it's worth reflecting on the limitations of references a bit more. We need to discuss the implications those restrictions have when we want our classes to contain reference variables.
If references cannot be reassigned, how can we use them in classes?
We might, for example, have a class like this:
class Pet {};
class Character {
public:
Pet& BestFriend;
}
This might seem like it falls foul of restriction 2, as we appear to have an uninitialized reference here - but not yet. We haven't created a Character
yet - this is just a class.
We have no uninitialised references because we have no Character
objects yet. Sure enough, if we create a Character
using this class, we'll see the error:
class Pet {};
class Character {
public:
Pet& BestFriend;
}
Character Goblin;
error: call to implicitly-deleted default constructor of 'Character'
Default constructor of 'Character' is implicitly deleted because field 'BestFriend' of reference type 'Pet &' would not be initialized
This error is somewhat indirect, but the chain of events is:
Character
class defines an uninitialsed referenceCharacter
class cannot initialise this variableWe'd want the BestFriend
variable to be different for each Character
, therefore, we'd need to initialise it from a constructor.
Unfortunately, with what we have learnt so far, we can't even do that:
class Pet {};
class Character {
public:
Character(Pet& Pet) {
BestFriend = Pet;
}
Pet& BestFriend;
}
error: constructor for 'Character' must explicitly
initialize the reference member 'BestFriend'
The issue here is that we are still not initialising BestFriend
due to a nuance on how constructors work. The member variables of our object are initialised before the body of our constructor runs.
So, in the above example, if our code compiled, when we create the object, we would be creating an uninitialised reference, and then we would be trying to reassign that reference in our constructor. This breaks both the restrictions of references!
Given these classes, how can we create a Goblin
?
class Sword {};
class Goblin {
Weapon& Sword;
}
So, we need to find a better way to create our constructors, such that the variables are initialised at the same time the object is.
This nuance of how objects are created has also been true of any of our previous constructors. We've been initialising the variables when we first create the object, and then reassigning them within the constructor.
That has been slightly suboptimal, but not especially problematic when dealing with simple numbers and booleans.
Once we have references in our class though, we are required to use a better way. That way is called a member initialiser list, and will be the topic of the next lesson!
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way