References (Lvalue References)
A reference lets you access a variable through another name, much like a pointer, but without needing a * to retrieve the value.
It’s not as powerful as a pointer, with no analogue for pointer arithmetic, but that trade-off is part of why it’s less error-prone.
What is a reference
A reference is just another name for an existing variable. In other words, it refers to the same memory location as that variable.
Line 5 (below) shows how to declare a reference, scoreRef, to the variable score. The & operator appearing next to the type means that this is a reference.
score and scoreRef are two names referring to the same memory location. Assigning a value to either of these changes the same int (lines 7–11).
Lvalue references and aliases
This kind of reference is also called an lvalue reference or an alias, and aliasing a variable just means giving it another name for the same storage.
lvalue vs. rvalue: An lvalue is an expression that refers to a memory location you can assign into, like a variable. An rvalue is just a value. In
x = 5;,xis an lvalue and5is an rvalue. The names come from ‘left value’ and ‘right value’, but an lvalue isn’t limited to the left side of an assignment statement. Inarr[0] = y;, botharr[0]andyare lvalues.
An lvalue reference normally binds only to an lvalue, which is why int& ref = x; compiles but int& ref = 5; doesn’t. The only exception is when it’s declared a const. A statment like const int& ref = 5; compiles, because the compiler holds the 5 in a temporary location for as long as the reference exists.
Declaring a reference (the fine print)
Unlike a pointer, which can stay uninitialized, a reference has to be bound to something the moment you declare it. This won’t compile:
int& badRef; // Error: a reference must be initializedIn a declaration, & only attaches to the next name. To create multiple references, either declare them on separate lines, or give each one its own &:
int x, y;int &r1 = x, &r2 = y;Even though &r1 looks like taking an address, it sits to the left of =, as part of r1‘s type. So it’s a reference.
References can’t be reseated
Once a reference is bound, it stays bound to that same variable for its entire lifetime. Unlike a pointer, you can’t reassign to it.
Think about what the following program will print:
Line 8 copies the value of b into a, because ref is just another name for a.
Passing by reference, revisited
A reference parameter is an ordinary reference used as a parameter. That reference is created and initialized with the argument passed to the function each time it’s called.
So when the reference parameter isn’t declared const, the argument has to be an lvalue:
Here, fixNegative gets a reference to measurements[2]‘s storage inside the vector.
A line like fixNegative(5); won’t compile, because 5 is an rvalue. On the other hand, the call print("hello") compiles for a function like void print(const std::string& s), because its reference parameter is declared const, which allows it to accept an rvalue.
Returning a reference to something that’s gone
Recall that an ordinary local variable’s lifetime ends once its function returns.
Returning a reference to such a variable is dangerous because it hands the caller a reference to an object that’s already destroyed.
value is a local variable, so its lifetime ends the instant getDefaultValue returns. Using result afterward is undefined behavior. It might print 100 (accidentally at that) or garbage.
While returning a reference to an ordinary local variable is a problem, it’s fine to return a reference to a variable with a longer lifetime than the function’s.
References vs pointers
Pointers can do everything references can, so it’s fair to wonder why C++ has both. The main reason is that references come with rules that make them harder to misuse.
Safety
A reference always refers to a real object, unlike a pointer. That guarantee is why a function with a reference parameter can count on its argument being real. A function that takes a pointer parameter has no such guarantee.
Collections
You also can’t collect references in an array or a vector:
int& temps[2] = {var1, var2}; // Error: no arrays of referencesstd::vector<int&> tempList = {var1, var2}; // Error: no vectors of referencesThat’s because an array or vector is a row of slots, each one a reserved memory location, whereas a reference is only a second name for a variable that exists elsewhere. So it makes no sense to set aside space for a reference.
A pointer is a variable in its own right, so a collection of pointers works fine:
int* temps[2] = {&var1, &var2}; // OKstd::vector<int*> tempList = {&var1, &var2}; // OKWhich to use
- Use a reference when you need to reach one particular variable. It’s the simpler and safer choice.
- Use a pointer, when you need to do pointer arithmetic or keep a collection that points to several variables.
Checkpoint
What does this print?
int x = 5;
int y = 10;
int& r1 = x, r2 = y;
r1++;
r2++;
std::cout << x << ' ' << y;Given the following code, which of these compiles?
int arr[3] = {1, 2, 3};
int total = arr[0] + arr[1];Is it safe to use the reference this function returns?
int& increment(int& n) {
n += 1;
return n;
}