Skip to main content

Welcome to a Grasp lesson

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

Lesson 4

Pointers vs. References in C++ Parameter Passing

Recommended resources

In our last lesson, we explored the RAII idiom, a powerful C++ principle for ensuring that resources are automatically managed by tying their lifetime to an object's scope. This lets you write safer and cleaner code, especially when dealing with C-style libraries that your whiteboard project will use. Now that you know how to create these robust resource-managing objects, the next logical step is to understand how to pass them—and other data—to functions effectively.

This lesson directly addresses that need by differentiating between two fundamental C++ mechanisms: C-style pointers and C++ references, specifically in the context of function parameters. While they can often achieve similar results, their syntax, semantic guarantees, and performance characteristics are distinct. For a developer focused on performant, low-level systems, mastering this distinction is not just about writing correct code, but about writing code that is also efficient and communicates clear intent.

The Core Difference: Address vs. Alias

At its heart, the difference between a pointer and a reference is simple:

  • A pointer is a variable that stores the memory address of another variable.
  • A reference is an alias—another name—for an existing variable.
This diagram illustrates the conceptual difference. The pointer `ptr` is a distinct variable that holds the address of `x`. The reference `ref` is simply another name for the variable `x` itself.

While the concept of an "alias" might seem abstract, a video from The Cherno provides an excellent, code-driven introduction. It demonstrates that references are essentially syntax sugar over pointers, designed to make code cleaner and safer.

REFERENCES in C++

This video starts by establishing the relationship between pointers and references, then demonstrates how a reference acts as a simple alias.

Watch the first segment from the introduction to understand the high-level relationship. Then, watch the example from creating a reference to see how modifying a reference directly modifies the original variable.

As the video explains, the reference ref doesn't truly exist as a separate entity in the compiled code in this simple case; it's just a way for you, the programmer, to refer to the original variable a.


Modifying Arguments in Functions

The most common use case where this distinction matters is when you want a function to modify a variable that was passed into it.

Let's consider a simple increment function. If you pass an integer by value, the function receives a copy, and any changes are lost when the function returns.

void increment(int value) {
    value++; // Modifies the local copy, not the original
}

int main() {
    int a = 5;
    increment(a);
    // 'a' is still 5
}

To solve this, you need to give the function access to the original variable.

  1. The C-style Pointer Approach: You pass the memory address of the variable. Inside the function, you use the dereference operator (*) to access and modify the value at that address.
  2. The C++ Reference Approach: You declare the function parameter as a reference. The compiler handles the underlying addressing automatically, and the syntax becomes much cleaner.

The Cherno's video continues by perfectly demonstrating this exact scenario.

REFERENCES in C++

This part of the video is crucial. It directly compares the pointer and reference solutions for creating a function that modifies its argument.

First, watch the section from explaining pass-by-pointer. This shows the classic C solution. Pay attention to the syntax (& at the call site, * in the function). Then, see how this is simplified from using a reference. Notice how much cleaner the code becomes both in the function body and at the call site.

As you saw, the reference-based version is syntactically identical to the (incorrect) pass-by-value version, but its behavior is what we want. This is a powerful feature for operator overloading and writing intuitive APIs.


The Low-Level Reality

Given your interest in systems programming, you might be asking: "If references are just syntax sugar, what do they look like at the machine code level?" This is an excellent question. In many cases, the compiler implements a reference as... a pointer. The key difference is that the compiler adds a layer of protection and handles the dereferencing for you implicitly.

The video "what even is a 'reference'?" provides a fantastic breakdown, including a look at the generated assembly.

what even is a "reference"?

This video demystifies references by showing you what's happening under the hood. It confirms that references are a compile-time concept built on top of pointers.

Watch the segment from this section on the internals. The speaker disassembles a small program to show that the code generated for using a reference is identical to the code for using a pointer. The "spicy pointers" analogy is quite apt: they are pointers with compiler-enforced safety rules.

This reveals the truth of C++ references: they are a zero-cost abstraction designed to provide the power of pointers (indirect access) without the syntactic overhead and some of the risks.


The Rules of Engagement: When to Use Which

Since references are compiler-managed pointers, they come with a stricter set of rules that define their idiomatic usage.

  • Nullability: A pointer can be nullptr, signifying that it points to nothing. A reference must be initialized to refer to a valid object and can never be null.
  • Reassignment: A pointer can be "reseated" to point to a different object after it's created. A reference is bound to its initial object for its entire lifetime; it cannot be changed to refer to something else.

This distinction is clearly demonstrated in the final part of The Cherno's video.

REFERENCES in C++

This segment highlights the critical difference in reassignability.

Watch from this explanation of why you cannot change what a reference refers to. Then, see the contrasting example from using a pointer to achieve the same goal.

These rules lead directly to the established C++ idioms for function parameters, which are excellently summarized in the image below.

This chart contrasts good and bad practices. The key takeaways for idiomatic usage are on the left: use a pointer for optional parameters, a reference for required parameters you intend to modify, and a const reference for large, read-only objects.

Let's break down the "GOOD PRACTICES":

  1. Optional Parameter (Pointer): If a function parameter is optional, a pointer (int* value) is the correct choice. The caller can pass nullptr to indicate "no value," and the function can check for this with if (value). This is a clear and explicit contract.
  2. Required Parameter (Reference): If a function requires a valid object to modify, a reference (int& value) is ideal. It guarantees to the function that it has received a valid object to work on (since references can't be null) and eliminates the need for null checks inside the function.
  3. Read-only Large Object (const Reference): If you are passing a large object (like a std::string, std::vector, or a complex Skia object) that the function only needs to read, pass it by const reference (const std::string& s). This avoids a potentially expensive copy while also preventing the function from accidentally modifying the object.

The "BAD PRACTICES" side highlights a critical danger: dangling pointers and references. This happens when a pointer or reference outlives the object it refers to. This is precisely the problem that RAII and smart pointers, which we covered in previous lessons, are designed to solve.


Performance Implications

A common belief is that passing by reference is always faster than passing by value because it avoids a copy. While true for large objects, it's not the whole story, especially in high-performance code.

For small, trivially copyable types (like int, double, or even a simple struct of two floats), passing by value can be faster.

  • Pass-by-value: The value can be placed directly into a CPU register. Access is instantaneous.
  • Pass-by-reference: A pointer to the value is passed (often in a register). To use the value, the CPU must perform an extra step: load the address from the register and then fetch the data from that memory address (an indirection). This can lead to cache misses and is fundamentally more work.

The article "Back to Basics: Pass-by-value vs. Pass-by-Ref" examines this with assembly code.

Back to Basics: Pass-by-value vs. Pass-by-Ref

This article explores the low-level performance trade-offs, which are critical for your project goals.

Read the section on performance. Pay close attention to the generated x86 assembly. You'll see the pass-by-reference version has an extra mov instruction to dereference the pointer, while the pass-by-value version can operate more directly.

The article also touches on another advanced performance topic: aliasing. When you pass by pointer or reference, the compiler may have to be more conservative with optimizations, because it can't be sure if two different pointers/references might actually be pointing to the same memory location. When passing by value, the function gets its own unique copy, which can enable more aggressive optimizations like vectorization.

For a comprehensive set of modern C++ guidelines, the Abseil team at Google provides an excellent "Tip of the Week".

abseil / Tip of the Week #234: Pass by Value, by Pointer, or ...

This document summarizes industry best practices for passing parameters, balancing safety, clarity, and performance.

Focus on the sections starting with "Guidelines". This provides a clear, rule-based approach for when to pass by value, by pointer/reference, or by using "view" types like string_view (a topic we may revisit).


Conclusion

You have now dissected the relationship between pointers and references, from high-level syntax down to their low-level implementation and performance trade-offs. This understanding is fundamental to writing C++ code that is not only correct but also communicates clear intent and performs efficiently.

Key Takeaways:

  • Reference as Alias, Pointer as Address: A reference is another name for an existing variable, while a pointer stores a memory address.
  • Clean Syntax for Modification: Pass-by-reference (T&) provides a much cleaner syntax than pass-by-pointer (T*) when a function needs to modify its argument.
  • Compiler-Enforced Safety: References are essentially compiler-managed pointers that cannot be null and cannot be reseated, making them inherently safer for many use cases.
  • Idiomatic Parameter Passing:
    • In-out (required): T&
    • In-out (optional): T*
    • Read-only (large/expensive): const T&
    • Read-only (small/cheap): T (by value)
  • Performance is Nuanced: For small, trivial types, pass-by-value is often more performant than pass-by-reference due to avoiding memory indirection. Always measure if it's critical.

In our next lesson, we'll continue our tour of modern C++ features by exploring auto type deduction and range-based for loops. These tools will help you write even cleaner, more expressive, and more maintainable code, building on the solid foundations we've established.