C++: The Basics - eps1.17_References
Hello and welcome. Well, have you recovered well since last time and reflected everything properly?
Very good, because we’re picking up seamlessly. We had been looking at pointers and mentioned one thing, but hadn’t really put it into context yet: references.
Now we want to be more specific and look at the difference as well as the distinction from Pointers.
What is a reference?
Without detours and directly summarized in one sentence:
A reference is a pseudonym for a variable.
So with a reference you can give a variable another name. Under this it is also known after declaration of the reference in the program.
Do you have a nickname, with which your friends call you? This can be compared well. No matter if your mother calls you by your birth name or your friends use your nickname. Each time, it’s you they’re referring to.
When you declare a reference, you must immediately initialize it with a variable. You don’t have a choice and you can’t connect them later. It creates a reference to that variable and another way to access its data.
At the declaration the reference operator &
is added to the datatype. This identifies the name that follows as the name of a reference.
Let’s play with one variable and two references for better understanding (see listing1).
// listing1: reference declaration
#include <iostream>
int main()
{
int anInt{15};
std::cout << "anInt = " << anInt << std::endl;
std::cout << "memory address of anInt = " << &anInt << std::endl;
int& refToInt = anInt;
std::cout << "reference to anInt = " << refToInt << std::endl;
std::cout << "memory address of reference to anInt = " << &refToInt << std::endl;
int& anotherRef = refToInt;
std::cout << "anotherRef = " << anotherRef << std::endl;
std::cout << "anotherRef is at address: " << &anotherRef << std::endl;
return 0;
}
# Output
anInt = 15
memory address of anInt = 0x7ffe5620f9f4
reference to anInt = 15
memory address of reference to anInt = 0x7ffe5620f9f4
anotherRef = 15
anotherRef is at address: 0x7ffe5620f9f4
listing1 shows you very nicely that a reference only introduces a new name for the same memory address and the data behind it.
The output on the console demonstrates that all references point to the same data. It doesn’t matter if they were initialized to the original variable or just to a different reference. The same memory address is always referenced.
Why are references useful?
References let you work with their initialized memory. So you can use them well in combination with functions. From eps1.15_Functions you know how a function is typically declared.
// listing2: declaring a function
// returnType FunctionName (type parameterName)
int toSquare(int number);
In listing2 you can see a typical example. If you call this function, it asks for an argument as parameter. This argument is copied into the parameter and occupies a block in memory. The function then works with the copy. Finally, the function returns a value. For this, its result is also copied. This time into the Return Value.
The copy operations increase the memory consumption of the program. Especially if the argument is very large.
There the question arises:
“Does the copying absolutely have to be?”
Wouldn’t it be much better if copying was omitted? Instead, the function could work directly with the data of the argument.
This is exactly what references are useful for.
Look at listing3. There you will find our example function int toSquare(int number)
and how it can be designed with references.
With void toSquareRef(int& number)
two differences are immediately noticeable. The function has the return type void
. Thus it does not return a value that would be copied. The argument is passed as reference. So it does not have to be copied anymore. The parameter thus becomes an alias for the argument, with which the function gets access to the data. For this procedure you can remember the keyword pass-by-reference.
// listing3: reference as function parameter
#include <iostream>
int toSquare(int number)
{
std::cout << "(memory address of argument = " << &number << ")" << std::endl;
number *= number;
return number;
}
void toSquareRef(int& number)
{
std::cout << "(memory address of argument = " << &number << ")" << std::endl;
number *= number;
}
int main()
{
int number = 10;
std::cout << "number = " << number << std::endl;
std::cout << "memory address of number = " << &number << std::endl;
std::cout << "The square of " << number << std::endl;
std::cout << "is: " << toSquare(number) << std::endl;
std::cout << "The square of " << number << std::endl;
std::cout << "is: ";
toSquareRef(number);
std::cout << number << std::endl;
return 0;
}
# Output
number = 10
memory address of number = 0x7ffff58c4da4
The square of 10
is: (memory address of argument = 0x7ffff58c4d8c)
100
The square of 10
is: (memory address of argument = 0x7ffff58c4da4)
100
The two functions have the same purpose. They are to square an integer number. While int toSquare(int number)
returns a copy of the result to the calling main
function, void toSquareRef(int& number)
overwrites the address range with the new data.
If you forget to mark the function parameter as reference with the &
operator in the declaration, the function will indeed perform its operation. But the result is not visible to main()
. The value of the variable does not change and the memory block remains untouched.
In this case, the result would be a local copy within the function, which is deleted when the function exits.
You should note, however, that the original value of the variable ‘number’ is lost in the variant with references. However, if you need the original value and the result, then you cannot avoid an additional variable and memory.
The keyword const
and references
References thus bring the advantage that functions do not make a copy of their parameters, but work directly with the arguments. And so the performance of your application increases noticeably.
As you probably already noticed, this ability also brings more responsibility for you. Because with a reference you get full access to the data of a memory address and you can change or even delete it.
However, if you do not want to manipulate the values, but only reuse them, then you should make sure that this does not happen unintentionally at any point.
Here references, which were declared with the keyword const
, help you. Like a constant, the initialized value of the reference cannot be changed or assigned anything as l-value. This will immediately result in an error message during the compile process.
// listing4: const reference
#include <iostream>
void toSquareRef(const int& number, int& result)
{
result = number*number;
// number *= number;
}
int main()
{
int number = 0;
int result = 0;
std::cout << "Enter a number: ";
std::cin >> number;
toSquareRef(number, result);
std::cout << "The square of " << number << std::endl;
std::cout << "is: " << result << std::endl;
return 0;
}
# Output
Enter a number: 5
The square of 5
is: 25
Unlike the previous example, the function in listing4 has two parameters. One brings the number to be squared and the other the result of the operation.
To be sure that the original number is not changed, the reference has been marked const
.
Don’t believe me? That is also good! Stay critical and check what is sold to you as new insight or knowledge. Otherwise you will easily fall for frauds and self-promoters.
So, what happens if you now assign a new value to the constant reference? Just try it out and comment the old operation of the function back in. What does the compiler think of this? Probably it complains that you try to assign a new value to a read-only
parameter.
Maybe this sounds a bit overcautious in the first moment. But at the latest in projects where other programmers reuse your code, it will increase the expressiveness and quality of your lines.
What is the difference between pointers and references?
You can now rightly ask the question:
“And what are references for? I already have pointers!”
And this question is justified. References and Pointers are very similar in their properties.
Pointers originate from the C programming language, which gave rise to C++. They are in both languages a possibility to refer indirectly to objects.
However, C++ additionally provides references as an alternative mechanism for basically the same task. In some situations C++ insists that you use pointers. Sometimes it makes sense to use references. Most of the time, it is left up to you to decide which notation to use. This makes it a matter of taste.
If this makes you feel insecure, stay calm anyway. You are not alone. Even if they don’t want to admit it, many C++ programmers don’t know exactly when which notation would be better.
Let’s lay out the differences.
Let’s start with the declaration. At first glance, the two look very similar. However, a reference uses the reference operator &
as an appendix to the data type and is initialized with a variable. A Pointer on the other hand is characterized by the Dereference operator *
and does not have to be initialized immediately. It can also be assigned the reference of a variable later in the program (see listing5).
// listing5: declarations
#include <iostream>
int main()
{
int anInt{15}; // variable initialization
int& refToInt = anInt; // reference intialization
int* pointToInt; // pointer declaration
pointToInt = &anInt; // pointer assignment
std::cout << "Value of variable = " << anInt << std::endl;
std::cout << "Address of variable = " << &anInt << std::endl;
std::cout << "Value of reference = " << refToInt << std::endl;
std::cout << "Address of reference = " << &refToInt << std::endl;
std::cout << "Value of pointer = " << *pointToInt << std::endl;
std::cout << "Address of pointer = " << pointToInt << std::endl;
}
# Output
Value of variable = 15
Address of variable = 0x7ffccb621f34
Value of reference = 15
Address of reference = 0x7ffccb621f34
Value of pointer = 15
Address of pointer = 0x7ffccb621f34
Initializing a reference with an object is often also called Binding, because the two are now connected.
The biggest difference can be seen very well in lisitng5. To dereference a pointer and get to the memory address data, you must explicitly use the dereference operator *
. After initialization, the dereferenced expression *pointToInt
of the pointer pointToInt
points to the value atInt
and does not return the address.
This is not necessary for references. refToInt
references the value of anInt
without an operator.
However, you cannot reference everything. The special memory area of nullptr
is not accepted by a reference. Of course you can somehow achieve this by bending and breaking. But what for? It only leads to errors and undefined behavior in your code.
This feature doesn’t necessarily make your code safer, it might make you aware of possible flaws.
When to point and when to refer
We now know that pointers and references have many similarities. Although pointers alone offer sufficient possibilities (see programming language C), over time a certain sense develops for when references are a useful addition. Even though there are always situations where the choice is not so clear. The important thing is that you develop a reasonably consistent philosophy for using references.
In your decision making, weighing them against the most important features will help. References can be associated with only one variable over their lifetime. They refer directly to the value, do not return the address, and cannot be a null reference.
Typically, functions do not need more of their argument to accomplish their task. So only use pointers where their properties are actually needed.
That sticks
References are aliases and are a useful alternative to Pointers when passing arguments to functions. They must be initialized, must not contain a nullptr
and thus guarantee valid values.
Since no copy of the argument is made, passing by reference is efficient, even when used with large structures or classes.
If it is useful, the function can change the value of the argument directly. Otherwise we have talked about const-correctness
and how the correct use of const
ensures that the value behind the reference does not change. Thus, the argument is effectively read-only
.
Even though pointers and references are not easy to understand, they add powerful functionality to our toolbox. With them we are well equipped to make our code faster, with optimal memory usage and more secure.
I wish you maximum success!
Sources
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017