Skip to content
C++
Language
since C++11
Beginner

Understand and Use References in C++

Learn to create and use C++ references so you can alias variables, avoid costly copies, and let functions modify caller data safely.

By the end of this page, you will be able to declare references, pass variables to functions by reference so those functions can modify them, and use const references to share large data without copying it. You will also recognise the most common reference mistakes and know how to avoid them.

What and Why

Every variable you create lives somewhere in memory. When you write int x = 5;, the program sets aside a small region of memory, labels it x, and stores the value 5 there.

A reference is a second name β€” an alias β€” for that same region of memory. It does not create a copy of the value; it just gives you another way to reach the exact same bytes.

cpp
int x = 5;
int& ref = x;   // ref is another name for x

After that second line, x and ref are the same thing. Changing one changes the other, because they refer to the same memory location.

Why does this matter?

Two situations come up constantly:

  1. Avoiding copies. If you have a large object β€” say, a vector with ten thousand elements β€” passing it to a function normally makes a full copy of all that data. A reference lets the function work with the original object directly, at almost zero cost.

  2. Letting functions modify caller data. Normally, a function cannot change a variable that belongs to the caller. With a reference parameter, it can.

Think of a reference like a nickname. If your name is "Alexander" and your friends call you "Alex", both names refer to the same person. Telling Alex to stand up and telling Alexander to stand up produce the same result.

Step by Step

Declaring a reference

The & symbol after a type name declares a reference type.

cpp
#include <iostream>

int main() {
    int x = 10;
    int& ref = x;          // ref is an alias for x

    ref = 20;              // modifies x through ref

    std::cout << x << "\n";   // prints 20
    std::cout << ref << "\n"; // prints 20
}

Two important rules apply here:

  • A reference must be initialised when it is declared. There is no such thing as an empty reference.
  • A reference cannot be reseated. Once it is bound to a variable, it stays bound to that variable for its entire lifetime.

References as function parameters

The most common use of references is in function parameters.

cpp
#include <iostream>

void addTen(int& n) {   // n is a reference to whatever the caller passes
    n += 10;
}

int main() {
    int score = 42;
    addTen(score);
    std::cout << score << "\n";   // prints 52
}

Without the &, addTen would receive a copy of score, modify that copy, and discard it β€” the original would stay at 42. The & makes the parameter a reference, so the function modifies the caller's variable directly.

Const references

When you want a function to read a value without copying it and without being able to change it, use a const reference.

cpp
#include <iostream>
#include <string>

void greet(const std::string& name) {   // read-only alias, no copy
    std::cout << "Hello, " << name << "!\n";
    // name = "other";   // compile error β€” const prevents modification
}

int main() {
    std::string username = "Ada";
    greet(username);
}

const std::string& is the idiomatic way to accept any string-like object in a function when you only need to read it. This pattern applies to any type that is expensive to copy.

Common Patterns

Pattern 1 β€” Swap two values

A classic demonstration of output parameters via reference:

cpp
#include <iostream>

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 1, y = 2;
    swap(x, y);
    std::cout << x << " " << y << "\n";   // prints 2 1
}

Both a and b are aliases for the caller's variables, so changes inside swap are immediately visible outside.

Pattern 2 β€” Range-based for loop with references

When iterating over a collection, using a reference in the loop avoids copying each element. Adding const prevents accidental modification.

cpp
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // Read each element without copying
    for (const int& n : numbers) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    // Modify each element in place
    for (int& n : numbers) {
        n *= 2;
    }

    for (const int& n : numbers) {
        std::cout << n << " ";   // prints 2 4 6 8 10
    }
    std::cout << "\n";
}

Pattern 3 β€” Returning a reference to a member

A class can expose its internals through a reference-returning function, allowing callers to read or write a member directly while the class retains ownership.

cpp
#include <iostream>

class Counter {
    int value_ = 0;
public:
    int& value() { return value_; }
    const int& value() const { return value_; }
};

int main() {
    Counter c;
    c.value() = 7;                         // write through reference
    std::cout << c.value() << "\n";        // prints 7
}

This pattern is common in container types (think of std::vector::operator[]).

What Can Go Wrong

Mistake 1 β€” Dangling reference

A reference must never outlive the variable it refers to. If the original variable is destroyed while the reference still exists, any use of the reference is undefined behaviour β€” the program may crash, produce garbage, or appear to work correctly until it doesn't.

cpp
int& danglingExample() {
    int local = 42;
    return local;   // WRONG: returning reference to a variable
                    // that is destroyed when the function returns
}

Fix: never return a reference to a local variable. Return references only to objects whose lifetime extends beyond the function call β€” class members, objects passed in by the caller, or objects with static storage.

Mistake 2 β€” Non-const reference cannot bind to a const variable or a literal

A non-const reference cannot bind to a const variable, a temporary value, or a literal. This surprises beginners:

cpp
void print(int& n) { /* ... */ }

print(5);             // compile error: cannot bind non-const ref to a literal
const int max {100};
print(max);           // compile error: cannot bind non-const ref to a const variable

The compiler rejects these because a non-const reference would let print modify the value β€” but you can't modify a literal or a const.

Fix: if the function only reads the value, make the parameter const int&. It will then accept literals, const variables, temporaries, and named variables alike.

cpp
void print(const int& n) { /* ... */ }

print(5);       // fine
print(max);     // fine

Mistake 3 β€” Assuming = reseats a reference

Because a reference is an alias, assigning to it modifies the original variable, not the binding.

cpp
int a = 1, b = 2;
int& ref = a;
ref = b;           // this sets a = 2, not "ref now refers to b"

// a is now 2, ref still refers to a

If you need something that can be rebound, use a pointer instead of a reference.

Quick Reference

SyntaxMeaning
int& r = x;r is an alias for x
void f(int& n)n is a reference parameter; function can modify caller's variable
void f(const int& n)read-only reference; no copy, no modification
for (int& e : v)iterate and modify elements of v in place
for (const int& e : v)iterate and read elements of v without copying

Key rules to memorise:

  • A reference must be initialised at the point of declaration.
  • A reference cannot be reseated after initialisation.
  • Never return a reference to a local variable.
  • Prefer const T& for function parameters when the function only needs to read the value.

What's Next

Now that you understand references, you are ready to explore the concepts they enable:

  • Pointers β€” a lower-level tool that can be reseated and can be null; understanding references first makes pointers easier to grasp.
  • Move Semantics β€” C++11 introduced rvalue references (T&&), which power efficient resource transfer.
  • const Correctness β€” a deeper look at how const interacts with references, pointers, and member functions throughout a codebase.
  • Function Overloading β€” now that you can write functions with reference parameters, learn how to provide multiple versions of the same function name.

References vs Pointers

References and pointers both let you refer to another object indirectly, but references have three hard constraints that make them safer and simpler in most situations. Once you know the differences by heart, you can choose the right tool for each job rather than defaulting to pointers out of habit.

The three constraints on references that pointers do not have:

  1. No null. A reference must refer to an actual object. There is no nullptr for references. A pointer, by contrast, can hold nullptr and you must always consider that possibility.

  2. No rebinding. Once a reference is bound to an object, it stays bound to that object for its entire lifetime. Assigning to the reference changes the object, not the binding. Pointers can be reassigned to point at a different object at any time.

  3. No arithmetic. You cannot advance a reference to the next element in an array. Pointer arithmetic (ptr++, ptr + 4) is a fundamental pointer feature with no reference equivalent.

cpp
int a = 1, b = 2;

// --- Pointer ---
int* p = &a;
p = &b;          // rebind: p now points to b
p++;             // arithmetic: move to next int in memory (dangerous!)
int* np = nullptr; // null is allowed

// --- Reference ---
int& r = a;
r = b;           // NOT rebind β€” sets a = 2
// int& nr;      // compile error: reference must be initialised
// r++;          // increments a, not a rebind β€” no pointer arithmetic

The rule of thumb: use a reference when the thing you refer to is guaranteed to exist and you do not need to rebind or do arithmetic. Use a pointer when you need nullability, rebinding, or array traversal.

PropertyReferencePointer
Can be nullNoYes
Can be reboundNoYes
Supports arithmeticNoYes
Must be initialisedYesNo
Syntax to access objectDirect (r)Dereference (*p)
Use & to take addressint& r = x;int* p = &x;

Const References and Lifetime Extension

A const reference can bind to a temporary object, and doing so extends the temporary's lifetime to match the reference's lifetime β€” a rule that appears in many real-world patterns and prevents a subtle category of bugs. Without this rule, const std::string& s = std::string("hello") would immediately produce a dangling reference, because the temporary std::string would be destroyed at the end of the expression.

cpp
#include <string>
#include <iostream>

int main() {
    const std::string& s = std::string("hello");
    // The temporary std::string lives as long as s does.
    // This is safe β€” the compiler guarantees it.
    std::cout << s << "\n";   // prints: hello
}

This is a deliberate language rule, not an accident. The lifetime extension applies only to const lvalue references β€” non-const references cannot bind to temporaries at all (the compiler would reject the code). It also applies to const T&& in certain situations, but that is an advanced topic.

The most common place you encounter this is in function parameters. A const T& parameter can accept a temporary returned from a function:

cpp
#include <string>
#include <iostream>

std::string make_greeting(const std::string& name) {
    return "Hello, " + name + "!";
}

void print(const std::string& msg) {   // binds to temporary
    std::cout << msg << "\n";
}

int main() {
    print(make_greeting("Ada"));   // temporary string flows through, safe
}

A non-const std::string& parameter would fail to compile here because it cannot bind to the temporary returned by make_greeting. This is exactly why you should reach for const T& when a function only needs to read its argument.

One important limit: lifetime extension does not chain. If you store a const T& to a member variable pointing at a temporary, the temporary is still destroyed at the end of the full expression β€” the member reference becomes dangling. Use this feature only for local variables and function parameters, not struct or class members.

Forwarding References and Reference Collapsing

In a template context, T&& does not mean "rvalue reference to T" β€” it means something more subtle called a forwarding reference, and understanding it requires knowing the reference collapsing rules. This topic is intermediate-level, but a first exposure here prepares you for template code you will read in the wild.

Forwarding references

When T is a template parameter and the parameter is declared as T&&, the compiler deduces T differently depending on what the caller passes:

cpp
template <typename T>
void wrapper(T&& arg) {   // T&& here is a forwarding reference
    // If caller passes an lvalue int, T is deduced as int&
    // If caller passes an rvalue int, T is deduced as int
}

int x = 5;
wrapper(x);    // T deduced as int& β€” arg binds to lvalue
wrapper(5);    // T deduced as int  β€” arg binds to rvalue

This is completely different from a non-template void f(int&& n), where n is always an rvalue reference and lvalues are rejected.

Reference collapsing rules

When the compiler substitutes a reference type for T in T&&, it gets int& &&, which is not valid syntax. The language applies reference collapsing to reduce it to a single reference:

T is deduced asParameter type T&& collapses to
intint&& (rvalue reference)
int&int& && β†’ int& (lvalue reference)
int&&int&& && β†’ int&& (rvalue reference)

The short version: any combination involving at least one & collapses to &; only && + && stays &&.

std::forward

The purpose of a forwarding reference is to pass the argument to another function while preserving whether it was an lvalue or an rvalue. std::forward does exactly this:

cpp
#include <utility>

template <typename T>
void outer(T&& arg) {
    inner(std::forward<T>(arg));   // preserves lvalue/rvalue-ness
}

Without std::forward, arg would always be treated as an lvalue inside the function body (because named variables are always lvalues), and the rvalue information would be lost. You will see this pattern extensively in standard library code and in any factory or wrapper function.

Common Mistakes with References

The most damaging reference mistakes are the ones the compiler cannot always catch: returning a reference to a local variable, and storing a reference to a temporary in a member. Both produce dangling references β€” references that point to memory that has been freed β€” leading to undefined behaviour that can be very hard to debug.

Returning a reference to a local variable

cpp
// WRONG β€” local 'result' is destroyed when the function returns
int& compute() {
    int result = 42;
    return result;   // compiler warning; using the returned reference is UB
}

int& r = compute();
std::cout << r;      // undefined behaviour β€” reading freed stack memory

The fix is to return by value, or to return a reference to something with a longer lifetime:

cpp
// CORRECT β€” return by value (cheap for int)
int compute() {
    return 42;
}

// CORRECT β€” return reference to caller-owned data
int& first_element(std::vector<int>& v) {
    return v[0];   // v's lifetime is the caller's responsibility
}

Storing a reference to a temporary in a struct member

cpp
struct Bad {
    const int& ref;
};

Bad b{42};   // 42 is a temporary β€” destroyed immediately
             // b.ref is dangling from this point on

Lifetime extension applies only to local const references, not to reference members of structs. The compiler may not warn you. The safe alternative is to store the value, not a reference:

cpp
struct Good {
    int value;    // stores a copy
};

Reference invalidation in containers

cpp
#include <vector>

std::vector<int> v = {1, 2, 3};
int& front = v[0];   // reference to first element

v.push_back(4);      // MAY reallocate the vector's internal buffer
// front is now potentially dangling β€” the buffer it pointed to may be gone
std::cout << front;  // undefined behaviour

Any operation that can reallocate a std::vector (push_back, insert, resize, reserve) may invalidate all references and iterators into it. If you need a stable reference into a vector, take it after all insertions are done, or use a container like std::deque that provides stronger iterator stability guarantees.

How References Appear in Compiled Code

At the machine code level, a reference is almost always implemented as a pointer β€” the compiler stores the address of the referred-to object and dereferences it wherever the reference is used. The language guarantees you a cleaner, safer interface, but under the hood the mechanism is the same.

You can verify this by looking at assembly. Consider:

cpp
void increment_ref(int& n) { n += 1; }
void increment_ptr(int* n) { *n += 1; }

On x86-64 with GCC, both functions produce nearly identical assembly β€” the int& parameter is passed in a register holding an address, just like int*. The only difference is that the compiler applied its safety rules (no null, no rebind) at the language layer before emitting code.

This has two practical implications:

  1. There is no runtime cost to using a reference. Passing const std::string& to a function is as cheap as passing a pointer β€” it transmits one address, not the entire string.

  2. A reference does not occupy storage of its own in the language model, but the compiler may allocate space for it when it cannot inline the reference away. In practice, a reference member of a class takes the same space as a pointer.

Practical Patterns: Const-Ref Parameters and Out-Parameters

Two patterns with references appear in almost every C++ codebase: passing large objects by const T& to avoid copies, and using non-const T& parameters as output channels from a function. Knowing when to use each one β€” and when to prefer return values instead β€” makes your function signatures immediately readable.

Passing large structs by const-ref

Any type larger than a pointer (typically larger than 8–16 bytes on 64-bit systems) should usually be passed as const T& rather than by value, unless the function needs its own copy:

cpp
#include <string>
#include <vector>

struct Employee {
    std::string name;
    std::string department;
    std::vector<std::string> skills;
    double salary;
};

// BAD: copies the entire Employee on every call
double bonus_pct_by_value(Employee emp) {
    return emp.salary > 100000.0 ? 0.15 : 0.10;
}

// GOOD: reads the employee without copying
double bonus_pct(const Employee& emp) {
    return emp.salary > 100000.0 ? 0.15 : 0.10;
}

The guideline from the C++ Core Guidelines: pass by value when the function needs to modify a copy; pass by const T& when the function only needs to read. For very small, trivially copyable types like int, double, or char, pass by value β€” the reference indirection can be slower than just copying the bytes.

Out-parameters via non-const reference

Before C++17's structured bindings and std::optional, the idiomatic way for a function to return more than one value was to write into reference parameters:

cpp
#include <string>
#include <cstdlib>

// Returns true if parsing succeeded; fills 'result' on success
bool parse_int(const std::string& text, int& result) {
    char* end;
    long val = std::strtol(text.c_str(), &end, 10);
    if (end == text.c_str()) return false;   // no digits consumed
    result = static_cast<int>(val);
    return true;
}

int main() {
    int n = 0;
    if (parse_int("42abc", n)) {
        // n is now 42
    }
}

This pattern still appears in performance-sensitive code and in APIs that must remain compatible with C++11/14. Modern code increasingly prefers returning std::optional<int> or std::pair/std::tuple, which make the intent clearer at the call site. Choose out-parameters when profiling shows the optional/tuple overhead matters, or when you are working in a codebase that has established this style.