References

This lesson demystifies references, explaining how they work, their benefits, and their role in performance and data manipulation
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a female blacksmith character
Ryan McCombe
Ryan McCombe
Edited

At this point, we’re hopefully familiar with calling functions, including passing arguments into our function’s parameters.

However, we may have noticed, that if we modify a parameter within our function, the original variable is left unaffected:

#include <iostream>
using namespace std;

void Increment(int x){ x++; }

int main(){
  int Number{1};

  cout << "Number: " << Number;
  Increment(Number);
  cout << "\nNumber: Still " << Number;
}
Number: 1
Number: Still 1

This is often the desired behavior, but not always.

In this lesson, we’ll understand why code like this doesn’t work in the way we might expect. Then, we’ll learn how to make it work.

Pass by Value

The behavior of our previous function is a result of how arguments get passed to parameters. When we call our functions, our arguments are copied.

So, the integer we’re modifying in our function is a copy of the original variable. The variable within the calling function is not modified.

This behavior is called passing by value and, by default, it is what is used with all variable types in C++. Below, we should a user-defined type, exhibiting similar behavior:

#include <iostream>
using namespace std;

class Character {
public:
  int Health{200};
};

void Combat(Character Player, Character Enemy){
  Player.Health -= 50;
  Enemy.Health -= 50;
}

int main(){
  Character Player;
  Character Enemy;
  cout << "Player: " << Player.Health;
  cout << "\nEnemy: " << Enemy.Health;
  Combat(Player, Enemy);
  cout << "\n\nPlayer: " << Player.Health;
  cout << "\nEnemy: " << Enemy.Health;
}

Because the Player and Enemy within our Combat() function are copies, the original Characters in our main() function are left unscathed:

Player: 200
Enemy: 200

Player: 200
Enemy: 200

Pass by Reference

The alternative to passing arguments by value is passing them by reference.

A variable and its corresponding reference point to the same location in our system’s memory. Therefore, modifying a variable through its reference will reflect in the original variable.

To denote a reference, we append an & to any of our other types. For example:

  • int& is a reference to an int
  • float& is a reference to a float
  • Character& is a reference to a Character, a user-defined type

A variable can be implicitly converted to a reference to a variable of that same type so, to update our function’s parameter to be passed by reference, we simply need to update its type using the & syntax:

#include <iostream>
using namespace std;

void Increment(int& x){ x++; }

int main(){
  int Number{1};

  cout << "Number: " << Number;
  Increment(Number);
  cout << "\nNumber: Now " << Number;
}

With that, modifications to the reference within the Increment() function are now reflected in our original variable in main():

Number: 1
Number: Now 2

We can do the same thing with our Combat() function, updating our Character types to be references, Character&:

#include <iostream>
using namespace std;

class Character {
public:
  int Health{200};
};

void Combat(Character& Player,
            Character& Enemy){
  Player.Health -= 50;
  Enemy.Health -= 50;
}

int main(){
  Character Player;
  Character Enemy;
  cout << "Player: " << Player.Health;
  cout << "\nEnemy: " << Enemy.Health;
  Combat(Player, Enemy);
  cout << "\n\nPlayer: " << Player.Health;
  cout << "\nEnemy: " << Enemy.Health;
}
Player: 200
Enemy: 200

Player: 150
Enemy: 150

Output Parameters

We’ve previously seen return statements in action. They’re the main way our functions communicate information back to their caller.

But with references, we now have another way. The caller can pass a value to our function as a reference, our function can modify it, and then the caller can inspect it after the function ends.

#include <iostream>
using namespace std;

int Attack(bool& wasFatal) {
  wasFatal = true;
  return 50;
}

int main() {
  bool wasFatal;
  int Damage{Attack(wasFatal)};

  cout << "Inflicted " << Damage << " Damage";
  if (wasFatal) { cout << " (Fatal)"; }
}
Inflicted 50 Damage (Fatal)

A parameter that is used for this approach is sometimes referred to as an output parameter.

Output parameters are rarely recommended. It’s difficult for our function to communicate which parameters will be used in this way, and the exact nature of the modifications that could be made.

The previous function can be written in a more friendly way by returning a new type. The caller can understand what the function returns just by looking at that type, and can even unpack its values using structured binding:

#include <iostream>
using namespace std;

struct AttackResult {
  int Damage;
  bool wasFatal;
};

AttackResult Attack(){
  return AttackResult{50, true};
}

int main(){
  auto [Damage, wasFatal]{Attack()};

  cout << "Inflicted " << Damage << " Damage";
  if (wasFatal) { cout << " (Fatal)"; }
}
Inflicted 50 Damage (Fatal)

But output parameters have some valid use cases, and the code we’re interacting with will often use them, so we should understand the pattern.

Passing Literals to References

When we set our function up to receive a reference, our function expects to be called with a variable from which to create that reference.

As such, we can no longer pass a simple literal, as attempted below:

void SomeFunction(int& x){}

int main(){
  SomeFunction(5);
}
error: cannot convert argument 1
from 'int' to 'int &'

We cover the nuance here in more detail in the advanced course.

For now, we can note that literals are temporary values and references are meant to refer to something more permanent, like a variable so that changes can be made to that variable.

If we’re not planning on changing the variable through our reference, we can bypass this error by declaring our reference as a constant, which we cover in the next section.

Reference Performance and const

Aside from the ability to modify parameters, passing by reference has a second, more useful property. Copying an object has a performance cost.

Therefore, if we don’t need a copy within our function, using pass-by-value may have an unnecessary performance overhead.

However, the ability to modify a reference in a way that affects variables within the calling code is generally not desirable. For this reason, we can mark references as constant, using const:

#include <iostream>
using namespace std;

class Character {
public:
  int Health{200};
};

void SomeFunction(const Character& Player){
  // We can do this with a const reference:  
  cout << Player.Health;

  // But we can't do this:
  Player.Health -= 50;
}

int main(){
  Character Player;
  SomeFunction(Player);
}
error: 'Health' cannot be modified because it is
being accessed through a const object

We can use the keyword const in many situations in C++, all relating to the idea of defining data that cannot be modified. We’ll see more examples later in the course.

With a const reference, we can read the data, but not change it. This includes indirect modification by, for example, passing the reference off to another function that hasn’t marked the parameter as const.

Constant references get us the performance benefits of passing by reference, whilst also letting the caller know we’re not going to modify their argument.

So how should I pass arguments?

The copying cost of trivial types such as int, float, and bool is negligible. Creating copies of these is approximately as fast as creating references to them, so we tend to just pass these by value.

The copying cost is something we need to be more conscious of when we’re dealing with objects that contain other objects.

Examples of this include user-defined types created from a class that defines multiple member variables. Strings are also an example of this, as a string is effectively a collection of char (character) objects.

As such, when we’re dealing with objects like these, passing them by const reference is generally preferred

Therefore, our decision process generally follows the following process:

  1. If we’re intentionally using an output parameter: pass by reference
  2. Else if the parameter is cheap to copy: pass by value
  3. Else if we need to modify the object in some way within our function, and don’t want our changes affecting the caller: pass by value
  4. Else: pass by constant reference

In real use cases, situations 2 and 4 are by far the most common.

Creating References

The use of references goes beyond just passing data to functions. We can use references at any time.

Below, we simply create a reference like any other variable. Our identifier Ref is bound to the MyNumber variable. As such, modifications to Ref affect MyNumber:

#include <iostream>
using namespace std;

int main(){
  int MyNumber{5};
  int& Ref{MyNumber};
  Ref++;
  cout << "Value: " << MyNumber;
}
Value: 6

Reference Restrictions

References closely relate to the idea of memory addresses, which we cover more throughout this chapter.

Working with memory addresses is a common source of bugs. References implement two restrictions that eliminate a large source of these defects:

References Must be Initialized

When we create a reference, we must also initialize it to point to the desired memory address - usually by assigning it to a variable:

int main(){
  int MyNumber{5};
  int& Ref{MyNumber};
}

If we try to declare a reference without providing it with an initial value, we will get a compilation error:

int main(){
  int& Ref;
}
error: 'Ref': references must be initialized

References Cannot be Reassigned

The second restriction is that a reference cannot be updated to point to a different memory address. Once a reference is created, it will always point to the memory address it was initialized with.

In the next lesson, we’ll introduce pointers. These use similar concepts to references, but they remove these two restrictions. This makes pointers more dangerous to work with, but also more powerful.

References as Class Members

Finally, let's see an example of a reference within a user-defined type. Below, our Character objects have a mSword, which is a reference to a weapon:

class Weapon {
public:
  string Name;
};

class Character {
public:
  Character(Weapon& Sword): mSword(Sword){}
  Weapon GetWeapon(){ return mSword; }

private:
  Weapon& mSword;
};

When our classes contain references, the references must be initialized within a member initializer list.

Attempting to set a reference within a constructor body is too late to satisfy the "references must be initialized" restriction. We covered the reasons for this in more detail in our earlier lesson:

References within an object work the same way as they do anywhere else. Below, we modify PlayerWeapon and, since our Character is holding that object by reference, we see the changes reflected in the Player.GetWeapon() return value:

#include <iostream>
using namespace std;

class Weapon {
public:
  string Name;
};

class Character {
public:
  Character(Weapon& Sword): mSword(Sword){}
  Weapon GetWeapon(){ return mSword; }

private:
  Weapon& mSword;
};

int main(){
  Weapon PlayerWeapon{"Wooden Sword"};
  Character Player{PlayerWeapon};

  cout << Player.GetWeapon().Name << '\n';
  PlayerWeapon.Name = "Steel Sword";
  cout << Player.GetWeapon().Name;
}
Wooden Sword
Steel Sword

A Note on Returning References from Functions

In the previous example, our GetWeapon() function is returning the weapon by value, not by reference.

Functions can return references, but we need to be careful here. Because a reference is linked to an underlying object, we need to ensure that the object has not automatically been deleted.

The following program creates a Character within the GetPlayer() function and returns a reference to it.

But when the function ends, the Character is automatically deleted, breaking the reference:

#include <iostream>
using namespace std;

class Character {
public:
  string Name;
};

Character& GetPlayer(){
  Character Player{"Anna"};
  return Player;
}

int main(){
  cout << "Name: " << GetPlayer().Name;
}

Our program will likely output some gibberish and then crash:

Name: P$÷P$÷^

We cover this scenario and how to deal with it in more detail later in this chapter.

Test your Knowledge

References

Consider the following variable:

int SomeVariable{42};

What is the correct syntax for declaring a reference to SomeVariable?

What is the result of modifying a variable through its reference?

What is the primary purpose of using references in functions?

What happens if you try to declare a reference without initializing it?

Summary

This lesson provided an introduction to the concept of references. We covered:

  • Understanding the difference between passing by value and passing by reference.
  • Learning how to use the & symbol to create references.
  • Exploring the use of references in functions, including output parameters and constant references.
  • Discussing the restrictions and rules governing the use of references.
  • Examining the role of references in user-defined types, such as class members.

Next Lesson: Pointers

The upcoming lesson will delve into pointers. Pointers have many similarities to references but with some additional capabilities. We’ll cover

  • Understanding the basic concept of pointers and their syntax.
  • Learning how to use pointers to directly access and manipulate memory.
  • Differentiating between pointers and references.
  • Exploring the use of pointers in functions.
  • Discussing best practices and common pitfalls associated with pointer usage.

Was this lesson useful?

Edit History

  • Merged and rewrote "Why we Need References" and "Creating References" lessons

  • First Published

Ryan McCombe
Ryan McCombe
Edited
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
References and Pointers
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 56 Lessons
  • Over 200 Quiz Questions
  • Capstone Project
  • Regularly Updated
  • Help and FAQ
Next Lesson

Pointers

This lesson provides a thorough introduction to pointers in C++, covering their definition, usage, and the distinction between pointers and references
3D art showing a female blacksmith character
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved