Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

References for Safer Pass-by-Reference

Recommended resources

Introduction

Hello! In our last lesson, we explored std::string and discovered a powerful C++ feature: passing by const reference (const std::string&). You saw how this avoids making slow, unnecessary copies of objects when calling functions. That was just a specific application of a more general and fundamental C++ concept.

Today, we're going to dive deep into references. This lesson directly addresses the learning outcome: to use references as a safer alternative to pointers for pass-by-reference. We'll define what references are, see how they simplify code, and compare them directly to pointers, which you may have encountered in other contexts.

Since you're interested in how things work "under the hood," we'll even look at how references are typically implemented at the machine level. This will give you a solid mental model for when to use a reference, when to use a pointer, and why C++ offers both.


1. What is a C++ Reference?

At its core, a C++ reference is an alias—another name for an existing variable. It's not a copy, and it's not a separate object in memory. When you create a reference, you are simply giving an existing variable a second name. Any action you perform on the reference is performed on the original variable.

REFERENCES in C++

To start, let's watch a segment from a video by The Cherno that introduces the concept of a reference and its basic syntax. He explains the idea of an alias very clearly.

Watch from the beginning to 3:54. Focus on how a reference is declared with the & symbol as part of the type and how modifying the reference changes the original variable.

As the video demonstrated, the syntax to create a reference looks like this:




#include <iostream>

int main() {
    int original = 100;
    
    // 'ref' is now an alias for 'original'. 
    // The & is part of the type declaration.
    int& ref = original; 

    std::cout << "Original: " << original << ", Reference: " << ref << '\n';

    ref = 200; // Modify the original variable through the reference

    std::cout << "After change -> Original: " << original << ", Reference: " << ref << '\n';

    return 0;
}

Output:

Original: 100, Reference: 100
After change -> Original: 200, Reference: 200

There are two iron-clad rules for references that make them distinct from pointers:

  1. A reference must be initialized when it is declared. You cannot have an uninitialized reference.
  2. Once a reference is initialized, it cannot be "reseated" to refer to a different variable. It will alias the original variable for its entire lifetime.

These rules are enforced by the compiler and are the foundation of why references are considered safer than pointers in many situations.


2. The Primary Use Case: Pass-by-Reference

The most common and important use for references is for function parameters. They allow a function to directly operate on the arguments provided by the caller, avoiding copies and allowing modifications.

Let's see the three ways to pass an argument:

  1. Pass-by-value: The function gets a copy. The original variable is unaffected.
  2. Pass-by-pointer (C-style): The function gets the memory address of the variable. It can modify the original by dereferencing the pointer.
  3. Pass-by-reference (C++ style): The function gets an alias to the variable. It can modify the original directly through the alias.

REFERENCES in C++

The next segment of the same video provides an excellent side-by-side comparison of these three methods using a simple increment function. Pay close attention to the syntax differences in both the function definition and the function call.

Watch from 3:54 to 7:03. This clearly illustrates the problem with pass-by-value and compares the pointer-based solution with the much cleaner reference-based solution.

Let's summarize the code from the video to make it crystal clear:

// 1. Pass-by-value (original is not modified)
void increment_by_value(int value) {
    value++; 
}

// 2. Pass-by-pointer (C-style)
void increment_by_pointer(int* value) {
    // Requires explicit dereference (*)
    (*value)++; 
}

// 3. Pass-by-reference (C++ style)
void increment_by_reference(int& value) {
    // Clean syntax, works on the original
    value++; 
}

int main() {
    int a = 5;
    increment_by_value(a);
    std::cout << "After by-value: " << a << '\n'; // Prints 5

    int b = 5;
    // Must pass the address (&)
    increment_by_pointer(&b); 
    std::cout << "After by-pointer: " << b << '\n'; // Prints 6

    int c = 5;
    // Pass the variable directly
    increment_by_reference(c); 
    std::cout << "After by-reference: " << c << '\n'; // Prints 6
}

Notice how the reference version provides the power to modify the original variable but with a much cleaner and less error-prone syntax than the pointer version. You don't need to remember to use & at the call site or * inside the function.


3. References vs. Pointers: An "Under the Hood" Look

You might be thinking that references and pointers seem to accomplish very similar things. You're right. In fact, under the hood, the compiler almost always implements a reference as a pointer. The key difference is the set of compile-time guarantees and the syntax that the C++ language provides.

A reference is like a const pointer that is automatically dereferenced for you.

what even is a "reference"?

This next video dives into the low-level details, even showing the assembly code generated by the compiler. This will confirm your intuition that references are essentially pointers with compiler-enforced safety rules.

Watch from 2:05 to 5:24. The first part lists the four main differences between pointers and references. The second part shows the assembly code to prove they are implemented similarly.

Let's combine this knowledge into a clear comparison.

Pointers vs. References in C++ Visual Comparison
This image visually contrasts a pointer, which is a distinct variable holding a memory address, with a reference, which is a direct alias for another variable.

Here are the key distinctions:

Feature Pointers (int* p) References (int& r) Why it Matters
Nullability Can be nullptr. Cannot be null. A function receiving a reference doesn't need to check for nullptr, eliminating null-dereference bugs. This is a huge safety win.
Initialization Can be uninitialized (dangerous!). Must be initialized at declaration. Prevents bugs from using uninitialized pointers that point to garbage memory.
Reassignment Can be changed to point to another object. Cannot be reseated to alias another object. Provides a guarantee that the reference will always refer to the same object throughout its lifetime.
Syntax Requires explicit dereferencing (*p) and address-of (&var). Syntax is clean; usage is implicit like a normal variable (r). Code is easier to read and write. The intent to modify is clear from the function signature.
Arithmetic Supports pointer arithmetic (p++). Does not support arithmetic. This is logical, as an alias isn't a separate memory location you can "move."

4. Practical Guidelines: When to Use Which?

So, when should you use a reference, a pointer, or just pass by value? Here are some widely accepted guidelines, based on best practices from the C++ community.

Tip of the Week #234: Pass by Value, by Pointer, or by Reference?

The following article from Google's Abseil library team provides excellent, professional-grade advice on parameter passing. We will focus on the guidelines for references and pointers.

Read the section titled "Pass by Reference or by Pointer". It gives clear rules for when to use each, especially regarding ownership and optional parameters.

Here is a summary of the core rules for passing parameters:

  • For "Input" parameters (read-only):

    • If the type is small and cheap to copy (like int, double, char), pass by value.
    • If the type is large or expensive to copy (like std::string, std::vector, or a large struct), pass by const reference (e.g., const MyClass&). This gives read-only access without a copy.
  • For "Output" or "Input/Output" parameters (needs modification):

    • If the parameter is required, pass by reference (e.g., MyClass&). The caller must provide a valid object, and the function's intent to modify it is clear.
    • If the parameter is optional, pass by pointer (e.g., MyClass*). The caller can pass nullptr to indicate "I don't have an object to provide," which is something a reference cannot do.

5. A Critical Warning: Dangling References

Just like pointers, references can become "dangling"—referring to memory that is no longer valid. The most common way this happens is by returning a reference to a local variable from a function.

C++ Pointers and References

Let's read a short section from a tutorial that clearly demonstrates this dangerous mistake.

Read the short subsection titled "You should not pass Function's local variable as return value by reference". The code example and explanation highlight exactly what not to do.

When a function finishes, its local variables are destroyed. If you returned a reference to one of them, that reference is now dangling and using it leads to undefined behavior.

Rule of Thumb: Never return a reference or a pointer to a local variable from a function.


6. Practical Exercise: Manipulating Game Objects

Let's apply these concepts. Since you're interested in games, we'll create a simple structure to represent a player and a function to move it.

  1. Create a file named references_practice.cpp.
  2. Define a struct named Player that contains two int members: x and y.
  3. Write a function void print_player(const Player& player) that takes a const reference to a Player and prints its coordinates in the format Player at (x, y).
  4. Write a function void move_player(Player& player, int dx, int dy) that takes a non-const reference to a Player and adds dx and dy to its x and y coordinates, respectively.
  5. In your main function:
    • Create a Player object and initialize its coordinates to (0, 0).
    • Call print_player to show its initial position.
    • Call move_player to move it by (5, -3).
    • Call print_player again to show its new position and verify that the original object was modified.

This exercise will solidify your understanding of passing by const reference for read-only access and by non-const reference for modification.


Conclusion

Today you've learned about one of C++'s most important features. References are not just syntactic sugar; they are a tool for writing safer, cleaner, and more expressive code.

Key Takeaways:

  • A reference is an alias for an existing variable.
  • They provide a clean syntax for pass-by-reference, allowing functions to modify caller variables without the cumbersome syntax of pointers (* and &).
  • References are safer than pointers because they cannot be null and must be initialized, which eliminates entire categories of common bugs at compile time.
  • Use const T& for large, read-only input parameters.
  • Use T& for required parameters that a function needs to modify.
  • Use T* for optional parameters, as pointers can be nullptr.

Next Up

We've now established that references are the preferred C++ way to alias stack-allocated objects or pass them to functions for modification. However, sometimes we need to create objects whose lifetime is not tied to the scope of a function. For this, we need to allocate memory on the heap. In the next lesson, we'll revisit pointers and learn the C++-specific way of managing dynamic memory using the new and delete operators.