References
Last lesson showed that with pass by value, a function gets a copy and can't change the variable outside. This lesson shows another way C++ offers: references. A reference is another name for a variable.
1#include <iostream>2 3int main() {4 int score = 80;5 int& ref = score;6 ref += 5;7 std::cout << score << '\n';8 std::cout << ref << '\n';9 return 0;10}
85 85
int& ref = score; reads "ref is a reference to score". An & after the type means a reference. ref is not a new variable and has no value of its own; it and score are two names for the same thing. So ref += 5 changes score. Press Step and see what ref points to.
One thing, two names
Whichever name you change it through, the other name sees the same:
1#include <iostream>2 3int main() {4 double price = 20;5 double& p = price;6 price = 25;7 std::cout << p << '\n';8 p = p * 0.8;9 std::cout << price << '\n';10 return 0;11}
25 20
Change price, and p sees 25; change p, and price becomes 20. A reference to an int variable is an int&, to a double a double&, to a std::string a std::string&.
Tip: int& ref and int &ref are the same; the space can go on either side. This course writes the & next to the type.
Bound at once, never changed
A reference must say whose name it is when it is declared. int& ref; on its own is a compile error: g++ says 'ref' declared as reference but not initialized.
Once bound, it is that variable's name for good; it can't be changed. A later ref = other_variable just copies the value over:
What does this program show?
1#include <iostream>2 3int main() {4 int x = 3;5 int y = 8;6 int& r = x;7 r = y;8 y = 100;9 std::cout << x << " " << r << '\n';10 return 0;11}
r stays a name for x. r = y doesn't make r a name for y; it stores the value of y, 8, in x. When y later becomes 100, that has nothing to do with x, so x and r are both 8.
Reference parameters
Where references really shine is function parameters. A reference parameter is another name for the variable given in the call, so changing it in the function changes the variable outside:
1#include <iostream>2 3void add_bonus(int& score) {4 score += 10;5}6 7int main() {8 int score = 80;9 add_bonus(score);10 std::cout << score << '\n';11 return 0;12}
90
One & different from last lesson, and the result goes from 80 to 90. Press Step: entering add_bonus adds a layer, but the score in it is not a new number; it points to the score in main.
The call doesn't show whether a parameter is a reference: add_bonus(score) looks the same either way. So give a function that changes its caller's variables a name that says so, like add_bonus or reset.
Swapping two numbers
Swapping the values of two variables is the classic use of reference parameters. With pass by value, the function would swap two copies; with references, it swaps the two variables outside:
1#include <iostream>2 3void swap_values(int& a, int& b) {4 int temp = a;5 a = b;6 b = temp;7}8 9int main() {10 int x = 3;11 int y = 8;12 swap_values(x, y);13 std::cout << "x = " << x << ", y = " << y << '\n';14 return 0;15}
x = 8, y = 3
Press Step and watch what a and b point to, and how temp helps: it keeps the value of a, which would otherwise be lost after a = b.
The standard library already has one: after #include <utility>, std::swap(x, y); does the same thing.
A reference needs a variable
An int& parameter wants a variable it can change. Give it a number, or the result of a calculation, and the compiler says no:
1#include <iostream>2 3void add_bonus(int& score) {4 score += 10;5}6 7int main() {8 int score = 80;9 add_bonus(90);10 std::cout << score << '\n';11 return 0;12}
main.cpp: In function 'int main()':
main.cpp:9:15: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
9 | add_bonus(90);
| ^~cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int': an int& can't be bound to a temporary value. 90 isn't any variable, and after adding 10 there would be nowhere to keep the result, so it isn't allowed. The same goes for add_bonus(score + 5).
const references
Pass by value makes a copy. Copying an int is cheap, but copying a long std::string (and later, containers holding thousands of elements) on every call is a waste.
Use a **const reference** instead: const std::string& text. The & means no copy, use the one outside; the const means the function only reads it, and changing it is a compile error.
1#include <iostream>2#include <string>3 4int count_char(const std::string& text, char c) {5 int n = 0;6 for (std::size_t i = 0; i < text.size(); i++) {7 if (text[i] == c) {8 n++;9 }10 }11 return n;12}13 14int main() {15 std::string fruit = "banana";16 std::cout << count_char(fruit, 'a') << '\n';17 std::cout << count_char("apple", 'p') << '\n';18 return 0;19}
3 2
A const reference has one more advantage: it can bind to a temporary value, so passing a string directly, as in count_char("apple", 'p'), works too. It won't be changed anyway, so there's no need for a place to keep it. (The std::size_t in the loop is the type size() gives: an integer that is never negative. More on it next lesson.)
Choosing how to pass
| You want to | Write | Examples |
|---|---|---|
| read it, small type | pass by value | int n, double price, char c, bool ok |
| read it, maybe large | const reference | const std::string& name |
| change the caller's variable | reference | int& score |
One function can mix them:
1#include <iostream>2#include <string>3 4void add_item(const std::string& name, double price, double& total) {5 total += price;6 std::cout << name << ": $" << price << '\n';7}8 9int main() {10 double total = 0;11 add_item("Milk", 6.5, total);12 add_item("Bread", 12, total);13 std::cout << "Total: $" << total << '\n';14 return 0;15}
Milk: $6.5 Bread: $12 Total: $18.5
name is only read, so it's a const reference; price is a small double, passed by value; total keeps adding up, so it's a reference.
Practice
Write the code, then press Check. A hint costs nothing; the answer is there when you need it.
Reset to zero
✓Write a function reset that uses a reference parameter to set the variable outside to 0. The program should show 0.
0
1#include <iostream>2 3int main() {4 int count = 42;5 reset(count);6 std::cout << count << '\n';7 return 0;8}
Smaller one first
✓Write a function sort_two(int& a, int& b): if a is bigger than b, swap them. After the call, low holds the smaller one. The program should show 4 9.
4 9
1#include <iostream>2 3int main() {4 int low = 9;5 int high = 4;6 sort_two(low, high);7 std::cout << low << " " << high << '\n';8 return 0;9}
Fix it: the curve that didn't count
✓apply_curve is meant to add 5 points, but the score is still 70. Add just one character so the program shows 75.
75
1#include <iostream>2 3void apply_curve(int score) {4 score += 5;5}6 7int main() {8 int score = 70;9 apply_curve(score);10 std::cout << score << '\n';11 return 0;12}
What you learned
- A reference is another name for a variable:
int& ref = score;. Changing it through either name is the same. - A reference must be bound when declared (
int& ref;is a compile error) and can't be rebound;ref = ycopies the value. - A reference parameter (
int& score) lets a function change the caller's variable; swap is the classic example, and the standard library hasstd::swap. - An
int&binds only to a variable, not to temporary values like90orscore + 5. const std::string&: no copy, no changes, and it accepts temporary values too; pass large things you only read this way.- Small types you only read: by value. Large ones you only read:
constreference. To change it: reference.