Previously, we introduced reference variables. These were a way to, indirectly, have two variables pointing to the same location in memory.
Working with memory addresses is often considered quite dangerous - it can introduce some nasty bugs if not managed correctly.
Because of this, references had two restrictions, designed to ward off the most common cause of those bugs:
However, sometimes our use case requires us to do one or both of these things. For this reason, we have pointers.
&
We can get the address of where a variable is stored in memory by using the &
operator. This is referred to as the address-of operator.
#include <iostream>
using namespace std;
int main(){
int x{1};
// Log out the location of x in memory
cout << &x;
}
The above program will output a memory address, which may look something like the following:
0x7ffe88a53e88
&
mean reference?Pointers and references are generally considered the most confusing topics learners encounter in their C++ journey, and the use of the same syntax to mean different things is one of the reasons why.
We’ve already seen &
being used to denote a reference. For example, a reference to an int
is an int&
.
Now we’re seeing &
being used for a different purpose. Confusingly, the &
syntax has a different meaning depending on where it appears in our code.
When we use &
next to a data type, we are saying we want to work with a reference to that data type:
int x { 5 };
// Create a reference to x
int& ReferenceToX { x };
When we use &
with a value that has an identifiable memory location, such as the name of a variable, &
is an operator. It is going to operate on that variable - specifically, it will get its memory address
int x { 5 };
// Find out where x is stored in memory
&x;
How can we find the memory address used by the isAlive
variable?
bool isAlive { true };
What gets returned from the &
operator is referred to as a pointer. It points to a location in memory. A pointer is a value like any other - it can be stored in variables, stored as members of a class, handed off to functions, and more.
A pointer type includes the underlying type (ie, the type of data being pointed at), and a *
suffix.
For example:
int
is an int*
bool
is a bool*
Character
(a user-defined type) is a Character*
For example, if we wanted a variable called MyPointer
to store a pointer to an int
, we could declare it like this:
int* MyPointer;
If we use the address of operator, &
on a variable containing an int
, we will get an int*
- a pointer to an int
We can store that like any other variable:
int x { 1 };
int* MyPointer { &x };
And we can pass them to functions:
void HandlePointer(int* Pointer) {
// ...
}
int main() {
int x{42};
HandlePointer(&x);
}
How can we create a variable called FloatPointer
to store a pointer to a float
?
With references, we could just work with the reference as if it were the base data type. We could use a reference to an int (an int&
) as if it were an int
.
We can't do that with pointers. To access the value that a pointer points to, we first need to visit the memory address the pointer is pointing at, and get the value stored there. This is referred to as dereferencing. the pointer and it is done using the *
operator.
The *
operator returns an object of the underlying type. For example, dereferencing an int*
(a pointer to an int
) will return an int
(a simple int
value)
#include <iostream>
using namespace std;
void HandlePointer(int* Pointer){
int Dereferenced{*Pointer};
cout << "Dereferenced: " << Dereferenced;
}
int main(){
int x{42};
HandlePointer(&x);
}
Dereferenced: 42
*
mean something else?Just like &
is used to mean different things, so too is *
.
When we use *
next to a data type, we are saying we want to work with a pointer to that data type:
// A pointer to an int
int* PointerToInt;
When we use *
with a variable that is a pointer, or an expression that returns a pointer, the *
then acts as an operator. Specifically, it is dereferencing that pointer, returning its underlying value:
// Dereference a pointer, returning the
// underlying value - an int, in this case
int x { *PointerToInt };
Previously, we covered how operators have different precedence. When we have multiple operators acting in a single expression, precedence controls what happens first.
For example, in an expression like 1 + 2 * 3
, multiplication happens first.
Precedence rules apply to non-arithmetic operators too, such as the dereferencing operator.
For example, the following code will not compile:
int main() {
int x{42};
int* Pointer(&x);
*Pointer++;
}
This is because the incrementing ++
operator has higher precedence than the dereferencing operator *
, so it happens first. It’s equivalent to this:
int main() {
int x{42};
int* Pointer(&x);
*(Pointer++);
}
The dereferencing operator has fairly low precedence in general, so when using it, we will often need to introduce brackets to ensure it happens first:
int main() {
int x{42};
int* Pointer(&x);
(*Pointer)++;
}
It's not important to know the precedence of all the operators. Even among professional developers, very few will have committed that to memory, because it's so easy to look up when it's needed.
The precedence of all operators in C++ is given here: https://en.cppreference.com/w/cpp/language/operator_precedence
The important thing to remember is the concept. If we see a bug in an expression that uses multiple operators, we should be aware that the problem might be caused by operator precedence, and we may need to add brackets.
To put all these concepts together, let's look at how we can get our Increment
function that we originally implemented with references to use pointers instead. Here's the version using a reference:
1void Increment(int& Number){
2 Number++;
3}
4
5int main(){
6 int x{1};
7 Increment(x);
8}
And here it is using a pointer:
1void Increment(int* Number){
2 (*Number)++;
3}
4
5int main(){
6 int x{1};
7 Increment(&x);
8}
The notable changes here are:
On line 1:
Our function no longer accepts an int&
(a reference to an integer). Instead, it now expects an int*
(a pointer to an integer)
On line 2:
Our function body can no longer treat Number
as an integer. It is now a pointer, so we need to dereference it before accessing or modifying the underlying value
On line 7:
We can no longer just pass an int
into our function and have the compiler implicitly convert it to the correct type for us automatically.
When using a pointer, we need to be more explicit. Therefore, we use the address of operator &
to ensure our function receives the pointer it expects.
Now that we've introduced pointers, it would be easy to dismiss references as being unnecessary. Pointers can fulfill the same need and more, given they are less restrictive.
However, references do have their benefits. The above code samples hopefully demonstrate that references are a bit easier to work with.
Additionally, the restrictions put on references are with good reason. In more complicated examples, the additional work of managing pointers can get a lot more complex.
So, it's a good practice to prefer references where possible. We should only use pointers when restrictions around references make them unusable for the specific problem at hand.
What should we put in the body of this function to double the value pointed at by the Number
pointer?
void Double(int* Number) {
// ??
}
->
When we’re working with pointers to our custom types, a common requirement we’ll have is to dereference the pointer, and then access one of its members.
The member access operator .
has higher precedence than the dereferencing operator *
, so we need to use brackets here:
void Combat(Monster* Enemy){
(*Enemy).TakeDamage(50);
}
C++ provides an alternative syntax for this, called the arrow operator. We can think of it as combining the dereferencing operator *
and the member access operator .
:
void Combat(Monster* Enemy){
Enemy->TakeDamage(50);
}
This is much more common, therefore, it will be our preferred approach going forward.
Note, that we only use the ->
operator with pointers to an object. When we have the actual object by value, or a reference to it, the .
is still used to access its members.
// We use . with objects
Character MyCharacter;
MyCharacter.TakeDamage(50);
// We use . with references
Character& Reference { MyCharacter };
Reference.TakeDamage(50);
// We use -> with pointers
Character* Pointer { &MyCharacter };
Pointer->TakeDamage(50);
Given the following code, what could we put on line 7 to call the Equip
function of the Weapon
passed in as a parameter?
class Weapon {
public:
void Equip() {}
};
void PrepareForBattle(Weapon SelectedWeapon) {
// ???
}
Given the following code, what could we put on line 7 to call the Equip
function of the Weapon
passed in as a parameter?
class Weapon {
public:
void Equip() {}
};
void PrepareForBattle(Weapon& SelectedWeapon) {
// ??
}
Given the following code, what could we put on line 7 to call the Equip
function of the Weapon
passed in as a parameter?
class Weapon {
public:
void Equip() {}
};
void PrepareForBattle(Weapon* SelectedWeapon) {
// ??
}
When working with pointers, we’ll often need a way to represent empty values.
For example, we may be making a game where our player can have a weapon:
class Weapon {};
class Character {
public:
Weapon* mWeapon;
};
To make a pointer point to nothing, representing the absence of a value, we use the nullptr
keyword:
class Weapon {};
class Character {
public:
Weapon* mWeapon{nullptr};
};
We should never attempt to dereference a nullptr
using the *
or ->
operator. If we need to dereference a pointer, and we think it may be a nullptr
, we can first check for that condition using an if
statement:
#include <iostream>
using namespace std;
class Weapon {
public:
string mName{"Iron Sword"};
};
class Character {
public:
Weapon* mWeapon{nullptr};
};
int main(){
Character Player;
Weapon Sword;
if (!Player.mWeapon) {
cout << "I am unarmed";
}
Player.mWeapon = &Sword;
if (Player.mWeapon) {
cout << "\nBut not any more! Behold my "
<< Player.mWeapon->mName;
}
}
I am unarmed
But not any more! Behold my Iron Sword
Finally, let's see a slightly more complex example. Our classes can contain references to other objects of the same class.
For example, a Character
can have a member variable that is a reference or a pointer to another Character
object.
This is the case for the mEnemy
variable within our Character
class below:
#include <iostream>
using namespace std;
class Character {
public:
Character(string Name): mName{Name}{}
void SetEnemy(Character* Enemy){
mEnemy = Enemy;
}
void LogEnemy(){
if (mEnemy) {
cout << "\nEnemy: " << mEnemy->mName;
} else {
cout << "\nI don't have an enemy";
}
}
string mName;
Character* mEnemy{nullptr};
};
int main(){
Character Player{"Anna"};
Player.LogEnemy();
Character Enemy{"Goblin Warrior"};
Player.SetEnemy(&Enemy);
Player.LogEnemy();
Character AnotherEnemy{"Vampire Bat"};
Player.SetEnemy(&AnotherEnemy);
Player.LogEnemy();
}
I don't have an enemy
Enemy: Goblin Warrior
Enemy: Vampire Bat
In this lesson, we explored the fundamental concepts of pointers. The key points include:
&
) and how it differs from a reference.>
) for member access in objects pointed to by pointers.nullptr
and ensuring safety checks before dereferencing.this
PointerIn our upcoming lesson, we'll delve into the this
keyword. This special pointer points to the object on which a member function operates.
By using this
, we unlock new possibilities and approaches in our code. Key Highlights of the Upcoming Lesson:
this
Pointer: We'll explore what this
is and how it inherently becomes part of every member function.this
can enable the chaining of methods, allowing for more fluent and readable code.this
can be used to compare objects or pass the current object to other functions.this
can be used to correctly overload operators that modify their operands, such as ++
and *=
.This lesson provides a thorough introduction to pointers in C++, covering their definition, usage, and the distinction between pointers and references
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way