C++ - c++ function - C++ Pass by reference

C++ Pass By Reference Explained

Updated June 2026

A function receives an integer, doubles it, and returns. The original variable stays unchanged. That behavior catches nearly every beginner off guard the first time they expect a modification to stick. The reason: the function was working on a copy, not the original.

Pass by reference C++ solves this. Instead of copying a variable into a function, a reference parameter gives the function direct access to the original. No duplication, no wasted memory, and any changes inside the function affect the caller’s variable immediately.

This article covers how pass by reference in C++ works, how it compares with pass by value and pass by pointer, when const references are the better choice, and where references are not the right tool.

What pass by reference means in C++

Pass by reference allows a function to operate directly on the original variable passed to it, rather than on a separate copy.

In C++, this is done by declaring a parameter with the & symbol. A parameter declared as int& b is a reference parameter. It acts as an alias for the original variable. It is not a new variable and it is not a copy. It is another name for the same data.

Any modification to the reference parameter inside the function changes the caller’s variable. That is the core behavioral difference from pass by value, where the function receives its own independent copy and the original stays untouched.

A reference must be bound to a variable at the point of the function call. It cannot be null, and it cannot be reassigned to refer to a different variable later.

Arguments and parameters

These two terms come up constantly when discussing C++ function parameters, and they refer to different things.

A parameter is the variable declared in a function’s signature. It defines what the function expects to receive.

A argument is the actual value or variable passed to the function at the call site.

Using the example from this article:

void duplicate(int& b);  // b is the parameter

duplicate(x);             // x is the argument

When duplicate(x) is called, the argument x is bound to the reference parameter b. Inside the function, b is just another name for x.

How pass by reference works in a function

Here is the core example:

#include <iostream>
using namespace std;

void duplicate(int& b) {
    b *= 2;
}

int main() {
    int x = 25;
    cout << "x before: " << x << endl;
    duplicate(x);
    cout << "x after: " << x << endl;
    return 0;
}

Output:

x before: 25
x after: 50

Here is what happens step by step:

  1. x is declared and initialized to 25.
  2. duplicate(x) is called. Because the parameter is declared as int& b, the reference b is bound to x. No copy is made.
  3. Inside the function, b *= 2 multiplies the value by 2. Since b is an alias for x, this modifies x directly.
  4. After the function returns, printing x shows 50.

The key point: b is not a separate variable that happens to hold the same value. It is x, accessed through a different name. That is why the change persists after the function ends.

If the parameter were declared as int b instead of int& b, the function would receive a copy. The original x would remain 25.

When pass by reference is most useful

References are not always necessary, but there are specific situations where they solve real problems. For most everyday C++ function parameters, references are the cleaner choice when a function needs to work with the original object and null is not part of the design.

When a function needs to modify the caller’s variable

This is the most common reason to use pass by reference in C++. If a function’s purpose is to change a value owned by the caller, a reference parameter makes that intent clear.

A function can also modify caller-owned data through a pointer. But when null is not a valid input, a reference expresses the intent more directly.

Common examples include update functions, swap-style operations, and functions that accumulate results into an existing object.

When passing large objects would be expensive

Copying a small int or double is cheap. Copying a std::string, a std::vector, or a custom class instance with many member variables is not.

Passing large objects by reference avoids that copy overhead. The function works with the original data directly rather than duplicating it into a new local variable.

The actual cost depends on the type and what the compiler optimizes. Avoid blanket claims that references are always faster. But for any object larger than a few bytes, passing by reference is a reasonable default. If the function only reads the object, a const reference is usually the better signature.

When working with polymorphism

Passing a derived-class object to a function that accepts a base-class parameter by value causes object slicing. The derived-class-specific data and behavior are stripped away, and only the base-class portion is copied.

Passing by reference avoids this. The function receives an alias to the original object, preserving its full type and behavior.

void display(core& c) {
    c.show();
}

When display is called with a derived-class object, the correct overridden show() function executes. This is a useful advanced case rather than the primary beginner takeaway, but it matters in any codebase that uses inheritance.

A practical example with a larger object

References are not just for integers. Consider a class that holds client data:

class ClientNumber {
public:
    int id;
    string name;
    string address;
    string phone;
};

A function that prints this object does not need to copy it:

void printNumber(ClientNumber& num) {
    cout << num.name << ": " << num.phone << endl;
}

Compare this to a pass-by-value version:

void printNumber(ClientNumber num) {
    cout << num.name << ": " << num.phone << endl;
}

The second version copies the entire ClientNumber object into a new local variable every time the function is called. For a small struct, the difference is negligible. For objects with strings, vectors, or other dynamically allocated members, the copy cost adds up.

If the function only reads from the object and does not modify it, the better signature is void printNumber(const ClientNumber& num). That avoids the copy and prevents accidental modification.

Pass by value vs. pass by reference vs. pass by pointer

Choosing between these three approaches depends on what the function needs to do with the data.

MethodWhat the function receivesCan modify original?Common use caseMain tradeoff
Pass by valueA copyNoSmall values, isolated logicCopy overhead for large objects
Pass by referenceAn alias to the originalYesUpdating data, avoiding copiesSide effects if used carelessly
Pass by pointerA memory addressYesOptional data, low-level APIs, nullable valuesMore complex syntax and null handling

Pass by value protects the original variable from unintended changes. The function works on its own copy, so nothing outside the function is affected. This is the safest default for small, primitive types.

Pass by reference keeps syntax cleaner than pointers and avoids copying. It is the preferred choice in most modern C++ when the function needs to read or modify the original object and null is not a valid input.

Pass by pointer uses address-of (&) and dereference (*) syntax. It allows a function to accept “no object” by passing nullptr. This is useful in C-style APIs, optional parameters, and cases where the pointer may need to be reassigned. The tradeoff is more complex syntax and the responsibility to check for null.

For most everyday C++ function parameters, prefer references over pointers when nullability is not part of the design.

What about const references?

A const reference gives a function read-only access to the original object without copying it.

void printName(const std::string& name) {
    cout << name << endl;
}

The parameter name refers directly to the caller’s string. No copy is made. But because it is declared const, the function cannot modify it. Attempting to change name inside the function results in a compile-time error.

This pattern is one of the most common in production C++. Use const references for:

  • Strings passed to display or logging functions
  • Vectors or containers passed to search or read operations
  • Custom class instances passed to functions that only inspect data

The rule of thumb: if a function does not need to modify the input and the type is larger than a primitive, const Type& is the standard signature. A non-const reference signals to the reader that the function will change the data.

Common mistakes beginners make

Most reference-related bugs come from a small set of recurring errors:

  • Forgetting the & in the parameter declaration. The function silently receives a copy instead of a reference. The original variable stays unchanged, and the bug can be hard to spot.
  • Expecting pass by value to modify the original. If the parameter is not a reference, the function cannot change the caller’s data. This is the most common source of confusion.
  • Thinking a reference parameter is a copy. A reference is an alias, not a duplicate. Changes to it affect the original directly.
  • Using a non-const reference when the function only reads data. This misleads other developers into thinking the function modifies its input. Use const& for read-only access.
  • Defaulting to pointers when references would be clearer. Pointers add null-checking complexity. If null is not a valid state, a reference is the simpler choice.
  • Passing a derived-class object by value to a base-class parameter. This causes object slicing and silently strips away polymorphic behavior.

Example output comparison

Seeing the output side by side makes the behavioral difference concrete.

Given int x = 25 and a function that multiplies the parameter by 2:

  • Pass by value: x after call = 25 (original unchanged, function modified a copy)
  • Pass by reference: x after call = 50 (original modified directly through the alias)
  • Pass by pointer: x after call = 50 (original modified through the dereferenced pointer)

Both reference and pointer approaches modify the original. The difference is syntax and intent. References use cleaner syntax. Pointers allow null and require explicit dereferencing with *.

When not to use pass by reference

References are a strong default for many situations, but not all.

  • Small primitive types like int, double, or char are cheap to copy. Passing them by value is often just as efficient and avoids any risk of accidental modification.
  • Non-const references create side effects. A function that modifies its input through a reference can make code harder to reason about, especially in larger codebases. If a function does not need to change the input, pass by value or const reference is clearer.
  • Functions that need to express “no object.” A reference must always be bound to a valid variable. If “nothing” is a valid input, a pointer or std::optional is the right tool.
  • Cases where the caller needs to retain an unmodified copy. Pass by value gives the function its own independent copy to work with, leaving the original untouched.

Choosing the right parameter style is about matching the function’s intent to the syntax. Not every parameter benefits from a reference.

Key takeaways

  • Use pass by reference C++ when a function must modify the caller’s variable directly.
  • Use references to avoid unnecessary copies of large objects like strings, vectors, and custom classes.
  • Prefer const references for large read-only inputs. This is the most common pattern in production C++.
  • Use pointers when nullability or reseating is part of the design.
  • Use pass by value for small primitive types when isolation from the caller’s data is the clearer choice.

Learn C++ online

Understanding how references work is a foundational skill for writing correct, efficient C++ functions. It connects directly to memory management, function design, and real project architecture.

To build on these skills with hands-on projects covering references, memory, object-oriented design, and more, explore the C++ Nanodegree program. You can also browse the full catalog of programming and development courses to find the right fit for where you are now. Consider joining our specialized courses.

Start Learning