So far, we’ve discussed two basic variable types:
- Normal variables, which hold values directly.
- Pointers, which hold the address of another value (or null) and their value can be retrieved through indirection of address they point to.
References are the third basic type of variable that C++ supports. A reference is a type of C++ variable that acts as an alias to another object or value.
C++ supports three kinds of references:
- References to non-const values (typically just called “references”, or “non-const references”), which we’ll discuss in this lesson.
- References to const values (often called “const references”), which we’ll discuss in the next lesson.
- C++11 added r-value references, which we cover in detail in the chapter on move semantics.
References to non-const values
A reference (to a non-const value) is declared by using an ampersand (&) between the reference type and the variable name:
1 2 |
int value{ 5 }; // normal integer int &ref{ value }; // reference to variable value |
In this context, the ampersand does not mean “address of”, it means “reference to”.
References to non-const values are often just called “references” for short.
Just like the position of the asterisk of pointers, it doesn’t matter if you place the ampersand at the type or at the variable name.
1 2 3 4 |
int value{ 5 }; // These two do the same. int& ref1{ value }; int &ref2{ value }; |
Best practice
When declaring a reference variable, put the ampersand next to the type to make it easier to distinguish it from the address-of operator.
References as aliases
References generally act identically to the values they’re referencing. In this sense, a reference acts as an alias for the object being referenced. For example:
1 2 3 |
int x{ 5 }; // normal integer int& y{ x }; // y is a reference to x int& z{ y }; // z is also a reference to x |
In the above snippet, setting or getting the value of x, y, or z will all do the same thing (set or get the value of x).
Let’s take a look at references in use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { int value{ 5 }; // normal integer int& ref{ value }; // reference to variable value value = 6; // value is now 6 ref = 7; // value is now 7 std::cout << value << '\n'; // prints 7 ++ref; std::cout << value << '\n'; // prints 8 return 0; } |
This code prints:
7 8
In the above example, ref and value are treated synonymously.
Using the address-of operator on a reference returns the address of the value being referenced:
1 2 |
cout << &value; // prints 0012FF7C cout << &ref; // prints 0012FF7C |
Just as you would expect if ref is acting as an alias for the value.
l-values and r-values
In C++, variables are a type of l-value (pronounced ell-value). An l-value is a value that has an address (in memory). Since all variables have addresses, all variables are l-values. The name l-value came about because l-values are the only values that can be on the left side of an assignment statement. When we do an assignment, the left hand side of the assignment operator must be an l-value. Consequently, a statement like 5 = 6;
will cause a compile error, because 5 is not an l-value. The value of 5 has no memory, and thus nothing can be assigned to it. 5 means 5, and its value can not be reassigned. When an l-value has a value assigned to it, the current value at that memory address is overwritten.
The opposite of l-values are r-values (pronounced arr-values). An r-value is an expression that is not an l-value. Examples of r-values are literals (such as 5, which evaluates to 5) and non-l-value expressions (such as 2 + x).
Here is an example of some assignment statements, showing how the r-values evaluate:
1 2 3 4 5 6 7 8 |
int y; // define y as an integer variable y = 4; // 4 evaluates to 4, which is then assigned to y y = 2 + 5; // 2 + 5 evaluates to 7, which is then assigned to y int x; // define x as an integer variable x = y; // y evaluates to 7 (from before), which is then assigned to x. x = x; // x evaluates to 7, which is then assigned to x (useless!) x = x + 1; // x + 1 evaluates to 8, which is then assigned to x. |
Let’s take a closer look at the last assignment statement above, since it causes the most confusion.
1 |
x = x + 1; |
In this statement, the variable x is being used in two different contexts. On the left side of the assignment operator, “x” is being used as an l-value (variable with an address). On the right side of the assignment operator, x is being used as an r-value, and will be evaluated to produce a value (in this case, 7). When C++ evaluates the above statement, it evaluates as:
1 |
x = 7 + 1; |
Which makes it obvious that C++ will assign the value 8 back into variable x.
The key takeaway is that on the left side of the assignment, you must have something that represents a memory address (such as a variable). Everything on the right side of the assignment will be evaluated to produce a value.
Note: const variables are considered non-modifiable l-values.
References must be initialized
References must be initialized when created:
1 2 3 4 |
int value{ 5 }; int& ref{ value }; // valid reference, initialized to variable value int& invalidRef; // invalid, needs to reference something |
Unlike pointers, which can hold a null value, there is no such thing as a null reference.
References to non-const values can only be initialized with non-const l-values. They can not be initialized with const l-values or r-values.
1 2 3 4 5 6 7 |
int x{ 5 }; int& ref1{ x }; // okay, x is an non-const l-value const int y{ 7 }; int& ref2{ y }; // not okay, y is a const l-value int& ref3{ 6 }; // not okay, 6 is an r-value |
Note that in the middle case, you can’t initialize a non-const reference with a const object -- otherwise you’d be able to change the value of the const object through the reference, which would violate the const-ness of the object.
References can not be reassigned
Once initialized, a reference can not be changed to reference another variable. Consider the following snippet:
1 2 3 4 5 |
int value1{ 5 }; int value2{ 6 }; int& ref{ value1 }; // okay, ref is now an alias for value1 ref = value2; // assigns 6 (the value of value2) to value1 -- does NOT change the reference! |
Note that the second statement may not do what you might expect! Instead of changing ref to reference variable value2, it assigns the value of value2 to value1.
References as function parameters
References are most often used as function parameters. In this context, the reference parameter acts as an alias for the argument, and no copy of the argument is made into the parameter. This can lead to better performance if the argument is large or expensive to copy.
In a previous lesson we talked about how passing a pointer argument to a function allows the function to perform indirection through the pointer to modify the argument’s value directly.
References work similarly in this regard. Because the reference parameter acts as an alias for the argument, a function that uses a reference parameter is able to modify the argument passed in:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> // ref is a reference to the argument passed in, not a copy void changeN(int& ref) { ref = 6; } int main() { int n{ 5 }; std::cout << n << '\n'; changeN(n); // note that this argument does not need to be a reference std::cout << n << '\n'; return 0; } |
This program prints:
5 6
When argument n is passed to the function, the function parameter ref is set as a reference to argument n. This allows the function to change the value of n through ref! Note that n does not need to be a reference itself.
Best practice
Pass arguments by non-const reference (rather than by pointer) when the argument needs to be modified by the function.
The primary downside of using non-const references as function parameters is that the argument must be a non-const l-value. This can be restrictive. We’ll talk more about this (and how to get around it) in the next lesson.
Using references to pass C-style arrays to functions
One of the most annoying issues with C-style arrays is that in most cases they decay to pointers when evaluated. However, if a C-style array is passed by reference, this decaying does not happen.
Here’s an example (h/t to reader nascardriver):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> #include <iterator> // Note: You need to specify the array size in the function declaration void printElements(int (&arr)[4]) { int length{ static_cast<int>(std::size(arr)) }; // we can now do this since the array won't decay for (int i{ 0 }; i < length; ++i) { std::cout << arr[i] << '\n'; } } int main() { int arr[]{ 99, 20, 14, 80 }; printElements(arr); return 0; } |
Note that in order for this to work, you explicitly need to define the array size in the parameter.
References as shortcuts
A secondary (much less used) use of references is to provide easier access to nested data. Consider the following struct:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Something { int value1; float value2; }; struct Other { Something something; int otherValue; }; Other other; |
Let’s say we needed to work with the value1 field of the Something struct of other. Normally, we’d access that member as other.something.value1
. If there are many separate accesses to this member, the code can become messy. References allow you to more easily access the member:
1 2 |
int& ref{ other.something.value1 }; // ref can now be used in place of other.something.value1 |
The following two statements are thus identical:
1 2 |
other.something.value1 = 5; ref = 5; |
This can help keep your code cleaner and more readable.
References vs pointers
References and pointers have an interesting relationship -- a reference acts like a pointer that implicitly performs indirection through it when accessed (references are usually implemented internally by the compiler using pointers). Thus given the following:
1 2 3 |
int value{ 5 }; int* const ptr{ &value }; int& ref{ value }; |
*ptr and ref evaluate identically. As a result, the following two statements produce the same effect:
1 2 |
*ptr = 5; ref = 5; |
Because references must be initialized to valid objects (cannot be null) and can not be changed once set, references are generally much safer to use than pointers (since there’s no risk of indirection through a null pointer). However, they are also a bit more limited in functionality accordingly.
If a given task can be solved with either a reference or a pointer, the reference should generally be preferred. Pointers should only be used in situations where references are not sufficient (such as dynamically allocating memory).
Summary
References allow us to define aliases to other objects or values. References to non-const values can only be initialized with non-const l-values. References can not be reassigned once initialized.
References are most often used as function parameters when we either want to modify the value of the argument, or when we want to avoid making an expensive copy of the argument.
![]() |
![]() |
![]() |
Why when we need a reference to array we use
not a
which means array of refs?
C++ doesn't support arrays of references. Array elements have to be objects, and references aren't objects in C++.
9.10 pointers and arrays is called 6.8 pointers and arrays here. Just a heads up!
1.
Now we know what this is :D
2. Section "References to non-const values":
> Just like the position of the asterisk of pointers, it doesn’t matter if you place the ampersand at the type or at the variable name.
You don't mention whether putting the ampersand next to the type or the identifier is preferred.
From the first pointer lesson:
> Best practice: When declaring a pointer variable, put the asterisk next to the variable name.
Could you include something like this in this lesson?
3. The syntax highlighter makes "ref" pink for some reason:
It makes the examples in this lesson a bit hard to read
4. Section "Using references to pass C-style arrays to functions":
> Here’s an example (h/t to reader nascardriver):
I think nascardriver is more than just a reader :P
-
Thanks for another great lesson, you explained references well :)
2. Pointers and references now have best practices for placing the *& next to the type. It will take a long time until this is used in all lessons.
3. Fixed
4. I'll leave it there for old time's sake :)
So is
preferable over
?
Thanks :)
It is now, on learncpp at least. Use whichever you prefer in your code, but use it consistently. I've added the best practices as to not confuse readers when some lessons use `int*` and others `int *`.
I might try running an auto-formatter across all code blocks, but that's going to have to wait until I'm very bored.
I'll stick to `int*` for consistency, thanks :)
Something that i would like to know " i double checked if you mentioned it in this tutorial but i couldn't find it " since a reference acts as a alias of a object and pretty much like pointer refers to that object, does that mean a reference doesn't take any memory from the stack? or it does?
in "Using references to pass C-style arrays to functions"
Note :that in order for this to work, you explicitly need to define the array size in the parameter.
this is very unfortunate. does this mean that our parameter can only accept specific arrays with number of elements matching the one specified in the parameter ?
i was able to pass an array by reference as a parameter but then std::size() wont work. i had to pass the array size as a separate parameter of type integer. i do hope there is a way for us to pass an array as reference without having to specify the size of the array in the parameter so that it is open to all arrays.
`std::span` is what you want. It's not covered on learncpp, but it's not hard to understand.
With regards to the following code:
This is a side effect, so I’m not sure why this would be recommended as a “best practice”.
I understand that function reference parameters help improve performance but at the cost of code readability and understandability.
We should aim for referential transparency and avoid side effects when possible.
I agree, non-const reference parameters aren't good, the "Best Practice" was misleading. I've added "rather than pointers", I think that's what the note was trying to say.
Thanks nascardriver. I know these pages are mainly reference material. It’s interesting how we learn all of these language features only to find out later on that they shouldn’t be used.
Other than purely C++ reference material, do you have any sections on good coding practices? I’m guessing it’s outside the scope of these tutorials, but for new programmers it could guide them in the right direction sooner rather than later. SOLID principles could be one example.
so, where should we place the ampersand preferably?
The lessons are doing this inconsistently.
Nice insight into learncpp's past: "Here’s an example (h/t to reader nascardriver)". So nascardriver was once just one of us readers. :-)
I didn't actually read the lessons :P I started replying to comments when I didn't have an active project and stuck around ever since, which lead to me contributing to the lessons and becoming an editor.
You seem like a cool guy, do you have a twitter account which we could follow? :)
Thanks! Nope, no twitter
Shouldn't the "Best practice: Pass arguments by non-const reference when the argument needs to be modified by the function." line be placed in a beautiful green Best practice box?
+1 beauty
PLease pick better variable names.
When you pick them like this it looks like it is part of the syntax.
int ref value.
Hi!
I updated the syntax highlighter, "ref" and "value" now look like all other variables.
very useful lesson
good job
In the example just below:
Using references to pass C-style arrays to functions
you define a function:
Why is there a need for a static_cast<int> and what is the consequence if it is left out. I don't see how the result of the std::size(arr) can be other than an int.
`std::size` returns an `std::size_t`, which is some unsigned integer type. Unsigned integers cannot be cast to signed integers in list initialization.
Isn't this part pointless? Since you have to declare the array size in the function header at compile time you don't need to calculate it since it's a constant.
`std::size` doesn't calculate the size of the array, it runs at compile-time and returns whatever size your declared the array with.
`std::size` only runs at run-time if the type you're giving at doesn't have a size that's known at compile-time. We get to such types later.
Hi,
I think this part "References can not be reassigned" needs to be reconsidered.
At least in qtcreator in linux using C++11, all the variable (value1, value2, ref) in the end get the same value of 6 !!!
That's the point of this example. Once a reference has been created, it cannot be changed to reference another variable. Assigning to a reference assigns to the referenced variable, not to the reference itself.
So, does that mean we MUST always use constant references?
No. I'm not saying "you're not allowed to change the reference", but "it's impossible to change the reference". In the example, when we do
we're not changing the variable that `ref` is referencing, we're modifying `value1`. This behavior isn't bad, just something to be aware of.
GREAT POINT !!!
I really haven't noticed that!
Thanks a million!