Learn C++ STL

Lesson 3 of 9 · From C to Just-Enough C++

Module 1 · From C to Just-Enough C++

References: A Second Name for the Same Box

FreeReading

In this lesson

  • Declare a reference and use it as a second name for a variable that already exists.
  • Write a function that changes its caller's variables through reference parameters, and one that reads a big value through const& without copying it.
  • State the three rules a reference keeps, read GCC 12's message when one is broken, and choose between a pointer and a reference.

Maria wrote a swap function in C last month. It took two pointers, put a star in front of every use, and needed an ampersand at every call. Bob forgot one star and spent an evening finding it. C++ has a way to write the same function with no stars and no ampersands at the call. It is called a reference, and every STL lesson after this one leans on it.

Maria's swap, in C and in C++

Here is Maria's C version. A function in C gets copies of its arguments, so to change the caller's x and y it needs their addresses.

#include <stdio.h>

void swap_ptr(int *a, int *b)
{
    int keep = *a;
    *a = *b;
    *b = keep;
}

int main(void)
{
    int x = 3;
    int y = 5;

    swap_ptr(&x, &y);
    printf("x = %d, y = %d\n", x, y);
    return 0;
}
x = 5, y = 3

And here is the same job in C++, with references.

#include <iostream>

void swapRef(int& a, int& b)
{
    int keep = a;
    a = b;
    b = keep;
}

int main()
{
    int x = 3;
    int y = 5;

    swapRef(x, y);
    std::cout << "x = " << x << ", y = " << y << '\n';
    return 0;
}
x = 5, y = 3

The output is the same. Three things moved, and the table names them.

C, with pointersC++, with references
The parameterint *aint& a
Using it inside*a = *b;a = b;
The callswap_ptr(&x, &y);swapRef(x, y);

So the C++ swap does the same work, with no address taken at the call and no star inside the function.

A reference is a second name for the same box

A reference is a second name for a variable that already exists. It is not a copy, and it is not a separate variable holding an address. Whatever you do to the reference, you do to the original box.

Declaring a reference

type& name = existing_variable;
  • type is the type of the box being named, such as int.
  • & after a type, in a declaration, means "reference to".
  • name is the new, second name.
  • existing_variable is the box it names, chosen once, at this line, for good.

Watch one box under two names.

#include <iostream>

int main()
{
    int score = 7;
    int& r = score;

    std::cout << "score = " << score << ", r = " << r << '\n';
    r = 8;
    std::cout << "after r = 8, score = " << score << '\n';
    score = 9;
    std::cout << "after score = 9, r = " << r << '\n';
    std::cout << "same box: " << (&r == &score) << '\n';
    return 0;
}
score = 7, r = 7
after r = 8, score = 8
after score = 9, r = 9
same box: 1

Writing through r changed score, and writing to score changed what r reads. The last line asks whether the two names have the same address. std::cout prints a true comparison as 1, so they do.

Here is the picture. On the left, one box carries both names. On the right, a pointer p = &score for comparison: the pointer is a second box, with its own address, holding the first box's address.

One int box at 0x7ffd10 holds 9 and carries two names, score and r; there is no second box for r.

score is a box at 0x7ffd10 holding 9; p is a second box at 0x7ffd18 holding 0x7ffd10, the address of score.

So r is not a copy of score and not a pointer to it. It is score, under a second name.

Why & means two things

Maria spotted it at once: & already meant "the address of" in C. Now it also means "reference to". Both are real, and the place the sign stands tells them apart.

#include <iostream>

int main()
{
    int x = 5;
    int& r = x;
    int* p = &x;

    std::cout << r << ' ' << *p << '\n';
    std::cout << (6 & 3) << '\n';
    return 0;
}
5 5
2
Where the & standsExampleIt means
after a type, in a declarationint& r = x;r is a reference to an int
in front of a variable, in an expressionint* p = &x;the address of x, as in C
between two values6 & 3bitwise AND, as in C: 110 AND 011 is 010, which is 2

So read the left side of the &. A type on its left makes a reference, nothing on its left takes an address, and a value on each side is bitwise AND.

Passing by reference: the function works on the caller's box

A function parameter can be a reference. Then the parameter is a second name for the caller's variable, for the length of the call. This is called passing by reference. The ordinary way, where the function gets its own copy, is passing by value.

Step through Maria's swapRef(x, y). When the call starts, a and b become second names for main's boxes, so nothing is copied in and nothing needs to be copied back.

main calls swapRef(x, y) with x = 3 and y = 5. Inside the call, a and b are second names for main's x and y, so when it returns, x is 5 and y is 3.

A reference parameter also gives a function a way to hand back more than one result. Kenji's till records each sale in two running numbers.

#include <iostream>

void addSale(int price, int& count, long long& total)
{
    count = count + 1;
    total = total + price;
}

int main()
{
    int count = 0;
    long long total = 0;

    addSale(120, count, total);
    addSale(80, count, total);
    addSale(45, count, total);
    std::cout << count << " sales, total " << total << '\n';
    return 0;
}
3 sales, total 245

price is passed by value, because the function only reads it. count and total are passed by reference, because the function must change main's copies.

Bob noticed something here. scanf("%d", &x) needs an ampersand, but std::cin >> x does not. The reason is this lesson: the >> that reads an int takes its right side as an int&. So std::cin writes straight into your box, and the ampersand disappears.

So a reference parameter lets a function work on the caller's own box, which is exactly what C did with a pointer, minus the stars.

The three rules a reference keeps

A pointer can be empty, can be changed to point elsewhere, and can be left without a value. A reference can do none of those. Each rule is checked by the compiler or shown by a run.

Rule 1: a reference is born naming a box. A reference with no box is refused.

int& r;

GCC 12 says, on the Playground's command line and with -Wall -Wextra alike: error: 'r' declared as reference but not initialized.

Rule 2: a reference is never null. There is no "reference to nothing" to start from.

int& r = nullptr;

GCC 12 refuses it at every command line: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'std::nullptr_t'. nullptr is C++'s null pointer, the NULL of C with a type of its own.

Rule 3: a reference never moves to another box. This one compiles, so watch what it does.

#include <iostream>

int main()
{
    int a = 10;
    int b = 20;
    int& r = a;

    r = b;
    r = 99;
    std::cout << a << ' ' << b << ' ' << r << '\n';
    return 0;
}
99 20 99

r = b; looks as if r now names b. It does not: it copies 20 into a, the box r has named since birth. So r = 99 also lands in a, and b keeps its 20.

So a reference is set once, at its declaration, to a real box, and it names that box until it goes out of scope.

const&: read a big value without copying it

Passing by value copies the argument. For an int that is four bytes, and nobody notices. For a list of a million numbers it is four million bytes, copied again on every call.

A const reference, written const type&, gives the function the caller's box under a read-only name. Nothing is copied, and the function promises not to change the box. The compiler holds it to that promise.

void show(const int& x)
{
    x = 7;
}

GCC 12, at every command line: error: assignment of read-only reference 'x'.

Here is the cost measured. The program builds a list of 1,000,000 int values with std::vector, an array that knows its own size; Module 2 teaches it. It sums the list 100 times through a by-value parameter, then 100 times through a const& one, and times both with <chrono>, the standard clock.

#include <chrono>
#include <iostream>
#include <vector>

long long sumByValue(std::vector<int> v)
{
    long long total = 0;
    for (int i = 0; i < (int)v.size(); i++) {
        total += v[i];
    }
    return total;
}

long long sumByConstRef(const std::vector<int>& v)
{
    long long total = 0;
    for (int i = 0; i < (int)v.size(); i++) {
        total += v[i];
    }
    return total;
}

int main()
{
    std::vector<int> big(1000000, 1);
    const int calls = 100;

    auto t0 = std::chrono::steady_clock::now();
    long long a = 0;
    for (int c = 0; c < calls; c++) {
        a += sumByValue(big);
    }
    auto t1 = std::chrono::steady_clock::now();
    long long b = 0;
    for (int c = 0; c < calls; c++) {
        b += sumByConstRef(big);
    }
    auto t2 = std::chrono::steady_clock::now();

    std::chrono::duration<double, std::milli> byValue = t1 - t0;
    std::chrono::duration<double, std::milli> byRef = t2 - t1;
    std::cout << "by value:     " << a << " in " << byValue.count() << " ms\n";
    std::cout << "by const ref: " << b << " in " << byRef.count() << " ms\n";
    return 0;
}

The times change from run to run, so they sit in a table rather than an output block. This is one run on Compiler Explorer, x86-64 GCC 12.2, at the Playground's flags, -O2 -std=c++17.

ParameterSum printed100 calls tookCopied per call
std::vector<int> v, by value10000000045.6 ms4,000,000 bytes
const std::vector<int>& v10000000016.8 msnothing

Both give the same sum. The by-value version is about 2.7 times slower, and every extra millisecond is copying: 400 MB moved over 100 calls to read numbers that never changed.

The usual advice, from the C++ Core Guidelines, rule F.16: pass small things such as int, double and char by value, and anything bigger by const&. So const& is how a function reads a big value: the caller's own box, read-only, at no copying cost.

A preview: the range-for by reference

Lesson 04 teaches the range-for, a loop that visits every box of an array in turn. One part of it belongs here, because it is a reference again.

#include <iostream>

int main()
{
    int marks[4] = {70, 85, 62, 91};

    for (int m : marks) {
        m = m + 5;
    }
    std::cout << "after a copy loop:      " << marks[0] << ' ' << marks[3] << '\n';

    for (int& m : marks) {
        m = m + 5;
    }
    std::cout << "after a reference loop: " << marks[0] << ' ' << marks[3] << '\n';
    return 0;
}
after a copy loop:      70 91
after a reference loop: 75 96

In the first loop, m is a copy of each box, so adding 5 to it changes nothing in marks. In the second, m is a second name for each box in turn, so the marks really change. One & decides it.

Pointer or reference: which one, when

References do not replace pointers. Each does a job the other cannot, and the table puts them side by side.

Pointer or reference, row by row Question Pointer: int* p Reference: int& r Can it be null? yes: p = nullptr no, never Can it move to another box? yes: p = &other no, set once at birth How you reach the box *p r, the plain name A box of its own? yes, with its own address no: &r is the named box's Arithmetic, p + 1? yes, along an array no In C? yes no, C++ only

Choose a reference for a parameter the function changes, and const& for a big one it only reads. Choose a pointer when "nothing" is a real answer, when it must walk along an array, or when the code is shared with C. The STL hands you references almost everywhere, and Module 11 shows the one pointer-like thing it adds: the iterator.

Example 1: Zara keeps a reading inside the sensor's range

Zara's thermometer reads from -40 to 60 degrees. Anything outside that is a glitch, so she pulls it back to the nearest end. The function changes the reading where it stands.

#include <iostream>

void clampTemp(int& t, int low, int high)
{
    if (t < low) {
        t = low;
    }
    if (t > high) {
        t = high;
    }
}

int main()
{
    int a = 25;
    int b = -55;
    int c = 70;

    clampTemp(a, -40, 60);
    clampTemp(b, -40, 60);
    clampTemp(c, -40, 60);
    std::cout << a << ' ' << b << ' ' << c << '\n';
    return 0;
}
25 -40 60

25 was inside the range and stayed. -55 and 70 were pulled to the ends. t is a reference, so each call changed main's own variable; low and high are only read, so they are plain int values.

Run in Compiler
Example 2: read a mark, and say whether it was valid

Bob keeps writing scanf. Here is the C++ shape of "read into my variable and tell me if it worked": the function fills a reference parameter and returns a bool.

#include <iostream>

bool readMark(int& mark)
{
    std::cin >> mark;
    return mark >= 0 && mark <= 100;
}

int main()
{
    int mark = 0;
    int good = 0;
    int bad = 0;

    for (int i = 0; i < 4; i++) {
        if (readMark(mark)) {
            good++;
        } else {
            bad++;
            std::cout << "rejected " << mark << '\n';
        }
    }
    std::cout << good << " good, " << bad << " bad\n";
    return 0;
}
rejected 120
rejected -3
2 good, 2 bad

That output is for the input 88 120 -3 45. mark in main is filled by the function, as scanf would fill &mark, so main can print the rejected value.

Run in Compiler
Example 3: Amara's sale price list

Amara reads n prices, takes a percentage off each one, and prints the list under the shop's name. One function changes a price, and one only reads the list and the name. In C++, const int MAX_N = 100; is a true constant, so it can size an array; the C track used #define because C does not allow that.

#include <iostream>
#include <string>

const int MAX_N = 100;

void applyDiscount(int& price, int percent)
{
    price = price - price * percent / 100;
}

void printList(const std::string& shop, const int prices[], int n)
{
    std::cout << shop << ':';
    for (int i = 0; i < n; i++) {
        std::cout << ' ' << prices[i];
    }
    std::cout << '\n';
}

int main()
{
    int prices[MAX_N] = {0};
    int n = 0;
    int percent = 0;
    std::string shop = "Amara's Books";

    std::cin >> n;
    for (int i = 0; i < n; i++) {
        std::cin >> prices[i];
    }
    std::cin >> percent;

    printList(shop, prices, n);
    for (int i = 0; i < n; i++) {
        applyDiscount(prices[i], percent);
    }
    printList(shop, prices, n);
    return 0;
}
Amara's Books: 400 250 120 90
Amara's Books: 300 188 90 68

That output is for the input 4, 400 250 120 90 and 25. prices[i] is one int box, so applyDiscount gets a second name for it and changes it. 250 less 25% is 187.5, and integer division keeps 188 (250 - 6250 / 100 = 250 - 62).

printList takes the name as const std::string&, a read-only name with no copy. The array arrives as a pointer, exactly as in C, and const promises the function will not write to it.

Run in Compiler

Where this is used

  • The standard std::swap. In <utility> it takes both arguments as references, the same shape as Maria's swapRef. Lesson 05 uses it on any type.
  • std::getline. It takes the string to fill as a reference, so std::getline(std::cin, line) writes into your own line (lesson 02).
  • std::cin >> x itself. The standard library's >> for an int takes an int&, which is why it needs no ampersand where scanf did.
  • The C++ Core Guidelines. The guidelines edited by Bjarne Stroustrup and Herb Sutter say: small inputs by value, bigger ones by const& (F.16), and in-out parameters by non-const reference (F.17).

Common mistakes

1. Forgetting the & in the parameter.

void swapValues(int a, int b)
{
    int keep = a;
    a = b;
    b = keep;
}

No message at any command line, the Playground's or -Wall -Wextra. The function swaps its own copies, and main still prints 3 5. Add the two ampersands: int& a, int& b. You will make this one because the only difference is one character, where C made you write stars everywhere.

2. Passing a plain number to an int& parameter.

swapRef(3, 5);

An error at every command line: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int', followed by note: initializing argument 1 of 'void swapRef(int&, int&)'. An lvalue is something with a box you can name; an rvalue, like 3, is a value with no box of its own. A reference needs a box, so put the values in variables first. The note line names the function and the argument, which is where to look.

3. Returning a reference to a local variable.

int& bad()
{
    int local = 42;
    return local;
}

GCC 12 warns even on the Playground's command line, with no -Wall: warning: reference to local variable 'local' returned [-Wreturn-local-addr]. local dies when the function returns, so the caller gets a name for a box that no longer exists. On Compiler Explorer, at the Playground's flags, printing it ended with Program terminated with signal SIGSEGV (11). Return by value: int good(). You will try it because returning a reference sounds like "returning without a copy".

4. A plain reference to a const variable.

const int limit = 10;
int& r = limit;

An error at every command line: error: binding reference of type 'int&' to 'const int' discards qualifiers. A writable name for a read-only box would break the promise const made. Write const int& r = limit; if you only read it.

Brain teaser

This program compiles with no message and prints 5.

#include <iostream>

int main()
{
    const int& r = 5;
    std::cout << r << '\n';
    return 0;
}

Remove the word const, and GCC 12 refuses with the message of mistake 2. A reference needs a box, and 5 is not a variable. So where is the box that r names, and why does const make the difference?

Ask what could go wrong if a writable name were given to the number 5 itself. Then ask what the compiler could quietly make, if the reader promises never to write.

Exercise 1Easy

The school adds a bonus to every mark after a hard paper. Write void addBonus(int& mark, int bonus), which adds the bonus to one mark but never takes it above 100, and call it on every box of the array.

Input. A line with n, a line with n marks, then a line with the bonus.

Output. The n marks after the bonus, on one line, separated by single spaces.

Constraints. 1 <= n <= 100. Each mark is between 0 and 100, and the bonus is between 0 and 100.

Sample. Input 3, 70 98 40 and 5 gives 75 100 45.

#include <iostream>

const int MAX_N = 100;

// Add bonus to mark, but never above 100.
void addBonus(int& mark, int bonus)
{
    // your code
}

int main()
{
    int marks[MAX_N] = {0};
    int n = 0;
    int bonus = 0;

    std::cin >> n;
    for (int i = 0; i < n; i++) {
        std::cin >> marks[i];
    }
    std::cin >> bonus;

    // Call addBonus on every mark, then print the marks on one line.

    return 0;
}

Not graded on its own. A vector would hold the marks more comfortably, and Module 2 teaches it; a plain array is enough here.

Run in Compiler
Exercise 2Medium

David wants the coldest and the warmest reading of the day from one function call. Write void minMax(const int a[], int n, int& lo, int& hi), which hands both answers back through its two reference parameters.

Input. A line with n, then a line with n integers.

Output. One line: the smallest and the largest, separated by one space.

Constraints. 1 <= n <= 100. Each integer is between -1000000 and 1000000.

Sample. Input 5 and 4 -2 9 0 9 gives -2 9.

#include <iostream>

const int MAX_N = 100;

// Put the smallest of a[0] to a[n - 1] in lo and the largest in hi.
void minMax(const int a[], int n, int& lo, int& hi)
{
    // your code
}

int main()
{
    int a[MAX_N] = {0};
    int n = 0;

    std::cin >> n;
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }

    int lo = 0;
    int hi = 0;
    minMax(a, n, lo, hi);
    std::cout << lo << ' ' << hi << '\n';
    return 0;
}

Not graded on its own. Zara would test n = 1 and a list of only negative numbers before the sample. Lesson 06 returns the same two answers as one pair, which is the graded min-max-pair.

Run in Compiler
Exercise 3Medium

Maria lines up three parcels on a shelf by weight, lightest on the left. She has only one move: look at two neighbours and swap them if the left one is heavier. The starter gives that move as void order(int& a, int& b). Write its body so that after a call a <= b, then use it to sort every line.

Input. The first line holds t. Each of the next t lines holds three integers a, b and c.

Output. t lines, each with the three integers of its input line in increasing order, separated by single spaces.

Constraints. 1 <= t <= 10000. Each integer is between -1000000000 and 1000000000.

Sample. Input 3, then 3 1 2, 5 5 1 and -1 -2 -3, gives 1 2 3, 1 5 5 and -3 -2 -1 on three lines.

#include <iostream>

// Put a and b in order: after the call, a <= b.
void order(int& a, int& b)
{
    // your code
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int t = 0;
    std::cin >> t;
    for (int i = 0; i < t; i++) {
        int a = 0;
        int b = 0;
        int c = 0;
        std::cin >> a >> b >> c;

        // Use order() to put a, b and c in increasing order,
        // then print them on one line, separated by single spaces.
    }
    return 0;
}

Graded as order-three. The hidden tests include all six orders of 1, 2 and 3, lines with two or three equal values, and 10000 lines at the limits. A parameter without its & passes none of them.

Run in Compiler

Common doubts

  • Is a reference just a pointer in disguise?

    Under the hood, a compiler often passes a reference parameter as an address, the way it would pass a pointer. But the language gives you none of a pointer's powers: no null, no moving, no arithmetic, no address of its own. Think of it as a name, because that is all your program can do with it.

  • How big is a reference?

    Ask sizeof and you get the size of the box it names. For double price = 9.5; and double& r = price;, a run on GCC 12 printed 8 8 for sizeof(r) and sizeof(price). The reference itself has no size you can ask about.

  • If const& saves a copy, why not pass everything that way?

    For an int or a double, the copy is a few bytes and costs nothing. Reading through a reference can even cost a little more, because the function must follow it. So small things go by value and big things by const&, as rule F.16 of the C++ Core Guidelines says.

  • Can I make an array of references?

    No. int& refs[2] = {a, b}; gives, at every command line, error: declaration of 'refs' as array of references. Each box of an array is an object of its own, and a reference is not an object, only a name.

  • Why does C not have references?

    They are C++'s addition. Bjarne Stroustrup, who created C++, wrote that he added them mainly to make operator overloading work, such as the >> of std::cin. Passing big things cheaply came with them.

Key takeaways

  • A reference, int& r = x;, is a second name for x: the same box, not a copy and not a pointer.
  • A reference parameter lets a function change the caller's variable, with no stars and no ampersand at the call.
  • const type& reads a big value with no copy, and the compiler refuses any write through it.
  • A reference is born naming a box, is never null and never moves: r = b; copies a value.
  • The & after a type makes a reference; in front of a variable it takes an address, as in C.
  • Use a pointer when "nothing" is an answer, when it must move along an array, or in code shared with C.

Next, lesson 04 lets the compiler write the types for you with auto, and finishes the range-for this lesson previewed.

End of lesson 3

Mark it done, and your progress moves with you.

Next: auto and the Range-for: Less Typing, Same Meaning