Menu

C++ References: Pass by Reference, const& and Aliases

C++ references explained - how the & in a parameter creates an alias, why pass-by-reference avoids copies and lets a function modify its caller's variables, when to reach for const& and references vs returning values.

This page includes runnable editors - edit, run, and see output instantly.

Another Name for the Same Thing

In the function parameters page, every argument was copied into the function. That copy is why a function can't change the caller's variable - it only ever sees its own duplicate. A reference breaks that wall. It is an alias: a second name bound to an existing variable, sharing the exact same memory.

You create a reference with & in the declaration. Once bound, the reference and the original are indistinguishable:

Two rules make references safe and predictable: a reference must be initialized the moment it's declared (int& r; is a compile error), and it can never be re-seated to point at something else afterwards. Assigning to a reference always writes to whatever it was originally bound to.

Pass by Reference: Let a Function Reach Back

The real payoff is in functions. Put & on a parameter and the function receives an alias to the caller's argument instead of a copy. Now changes inside the function are visible outside it:

Drop the & and addBonus would bump a throwaway copy, leaving total at 100. The single character is the whole difference. This is the canonical way to write a function that returns more than one result or edits its input in place. The classic example is swapping two variables:

Without references, swapValues would only swap local copies and x/y would stay 1 2. (The standard library already has std::swap, but writing it yourself shows exactly what a reference parameter buys you.)

const References: Read Fast, Promise Not to Touch

Pass-by-reference also avoids copying - and for a big object, that copy can be expensive. But a plain T& parameter signals "I might modify this," which is misleading when you only want to read. The fix is const T&: you get the no-copy speed of a reference and a compiler-enforced promise that the function won't change the argument.

A non-const reference can only bind to a modifiable variable, but a const reference can also bind to literals and temporaries - that's why greet("literal works too") compiles. A practical rule of thumb for choosing a parameter type:

void f(int x)              // cheap type, read-only -> just copy it
void f(const string& s)   // heavy type, read-only -> const reference
void f(string& s)         // you intend to modify the caller's object

Default to const T& for any class type you only read (string, vector, your own structs), and reserve a non-const reference for when you genuinely mean to write back.

Returning a Reference

A function can also return a reference, handing the caller an alias to something that already exists. This is common in container-like code - it's what lets v[i] = 5 work and what operator overloading does under the hood:

Because at returns int&, the call expression at(data, 1) is itself an lvalue you can assign to. Return a plain int instead and at(data, 1) = 42 would fail to compile - you'd be assigning to a temporary copy.

The Big Gotcha: Dangling References

A reference does not own anything; it just points at memory that lives elsewhere. If that memory dies while the reference is still in use, you have a dangling reference, and reading through it is undefined behavior - it may print garbage, crash, or appear to work until it ruins your day in production. The classic mistake is returning a reference to a local variable:

int& broken() {
    int local = 42;
    return local;   // BUG: local is destroyed when broken() returns
}                   // the returned reference dangles

int main() {
    int& r = broken();
    cout << r << "\n";   // UNDEFINED BEHAVIOR - reads dead memory
}

The local local is gone the instant broken returns, so the reference points at reclaimed stack space. Only return a reference to something that outlives the call - a parameter passed in by reference, a data member, or a static. If the value is computed inside the function, return by value instead and let the compiler optimize the copy away. The same trap hits range-based loops and any reference bound to a temporary: never hold a reference past the lifetime of the thing it names.

Next: Function Overloading

References give you a second knob on every parameter - copy vs. alias, mutable vs. const - and that knob interacts directly with the next topic. Up next, function overloading lets you define several functions with the same name but different parameter lists, and the compiler picks the right one by matching argument types - including whether they're passed by value, by reference, or by const reference.

Frequently Asked Questions

What is a reference in C++?

A reference is an alias for an existing variable - another name for the same memory. You create one with & in the declaration: int& r = x;. After that, r and x are interchangeable; changing one changes the other. References must be initialized when declared and can never be re-seated to refer to a different variable.

What is the difference between pass by value and pass by reference in C++?

Pass by value (void f(int x)) copies the argument, so the function works on its own copy and the caller's variable is untouched. Pass by reference (void f(int& x)) gives the function direct access to the caller's variable, so changes are visible after the call - and no copy is made, which matters for large objects.

When should I use const reference parameters in C++?

Use const T& when a function only needs to read a parameter but the type is expensive to copy (string, vector, big structs). You get the no-copy speed of a reference plus a compiler guarantee that the function won't modify the caller's value. For cheap types like int or double, plain pass-by-value is simpler and just as fast.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED