Chapter 10
std::vector: A List That Grows
push_back, indexing, and the loop that reads the whole thing.
Chapter 9 left you with a roll function and two dice. Roll ten and you need ten
variables. Roll a thousand and the idea collapses. You can’t type a thousand
names, and even if you did, you couldn’t loop over them.
What you want is one name holding many values.
A list you can add to
#include <iostream>
#include <vector>
int main() {
std::vector<int> rolls;
rolls.push_back(4);
rolls.push_back(6);
rolls.push_back(1);
std::cout << "count: " << rolls.size() << '\n';
}
count: 3
std::vector<int> rolls; makes an empty list of int. Empty, as in genuinely
containing nothing: rolls.size() right after that line is 0.
push_back puts a value on the end and the vector gets bigger. There is no
maximum you declare up front and no point where it fills up; it makes room as it
goes. That is the whole trick, and it is why you will reach for a vector far more
often than anything else in this book.
size() is a dot function, like full.size() from chapter 4, and for the same
reason: the vector is carrying that number around with it.
Getting at what’s inside
Square brackets, and a number:
std::cout << "first: " << rolls[0] << '\n';
std::cout << "last: " << rolls[rolls.size() - 1] << '\n';
first: 4
last: 1
Positions start at 0, exactly as string positions did in chapter 4, and for once
the two things you have learned agree with each other. Three elements means
positions 0, 1 and 2, so the last one is at size() - 1: the same arithmetic,
the same off-by-one waiting for you.
You can assign through the brackets too. rolls[0] = 5; replaces the first value.
That only works for positions that already exist; brackets never make the vector
bigger, only push_back does.
The loop for reading all of it
You could walk a vector with the for loop from chapter 8, and sometimes you
have to. Most of the time you want this instead:
std::vector<std::string> names;
names.push_back("Ada");
names.push_back("Grace");
names.push_back("Alan");
for (std::string name : names) {
std::cout << name << '\n';
}
Ada
Grace
Alan
Read the brackets as “for each name in names”. No counter, no condition, no
++, and no opportunity to get the last position wrong. Each time round, name
holds the next element.
This is called a range-for, and it is what you should write whenever you want every element and don’t care where they sit. Use the counting loop when you need the position itself. Printing “3. Alan” needs a number, and range-for hasn’t got one.
Two things worth noticing about that example. The vector holds std::string this
time, and nothing else changed: push_back, size and the brackets all behave
the same. The <int> slot really is a slot.
And name is a copy of the element, so changing it inside the loop changes
nothing in the vector. Same rule as function parameters in chapter 9, same fix in
chapter 12, which comes back to this exact loop and improves it.
An empty vector is worth a moment too. Run a range-for over one and the body simply never executes, which is almost always what you wanted. No special case to write.
Filling one from the keyboard
push_back doesn’t care where the value came from, so a loop and a cin are
enough to collect input you couldn’t have counted in advance:
std::vector<int> scores;
for (int i = 0; i < 3; ++i) {
std::cout << "Score " << i + 1 << ": ";
int score = 0;
std::cin >> score;
scores.push_back(score);
}
std::cout << "You entered " << scores.size() << " scores.\n";
Note i + 1 in the prompt. The loop counts from 0 because the positions do, but
nobody wants to be asked for “Score 0”, so the display gets the adjustment and the
vector doesn’t. Doing it the other way round, counting from 1 and subtracting
when you index, is how off-by-one bugs get in.
This version asks for exactly three. Letting the reader stop whenever they like means noticing when input runs out, which is chapter 17’s job.
Vectors go in and out of functions
A vector is a value like any other, so it goes through the brackets of a function
the same way an int does: in as a parameter, out as a return.
std::vector<int> first_squares(int how_many) {
std::vector<int> results;
for (int i = 0; i < how_many; ++i) {
results.push_back(i * i);
}
return results;
}
int total(std::vector<int> numbers) {
int sum = 0;
for (int n : numbers) {
sum += n;
}
return sum;
}
int main() {
std::vector<int> squares = first_squares(5);
std::cout << "size: " << squares.size() << '\n';
std::cout << "total: " << total(squares) << '\n';
}
size: 5
total: 30
first_squares builds a vector that didn’t exist before and hands it back. The
one inside the function stops existing at the closing brace, exactly as chapter 9
said, but the value has already been returned by then.
total is the shape you will write over and over: take a vector, walk it with a
range-for, accumulate, return one number. Length, average, largest, how many are
negative, all the same five lines with the middle changed.
Both of these copy the whole vector, which for five numbers costs nothing and for a million is real work you didn’t ask for. Chapter 12 is about that, and about the one small change that fixes it.
Exercise 1 · Fill one and read it back
Make a std::vector<int>, push the numbers 1 to 5 into it with a for loop,
then print them with a range-for.
Now print them with a counting loop instead, as 1. 1, 2. 2, and so on. You
need the position, so this is the case range-for can’t do.
Finally add up all five with a range-for and a running total, and print the sum. You should get 15.
Check yourself
Project
A thousand dice
Roughly 45 minutes
Chapter 8’s stretch let you roll a random number and chapter 9 gave it a name. One roll tells you nothing. A thousand tells you what a die actually does.
The program rolls a six-sided die 1,000 times, counts how often each face came up, and prints the tally.
1: 167
2: 173
3: 148
4: 170
5: 178
6: 164Bring roll across from chapter 9 unchanged. Then:
std::vector<int> counts(7, 0);Seven slots, not six, so that face 4 lives at counts[4] and you never do
arithmetic on the index. Slot 0 goes unused, and wasting one int to make the
rest of the program obvious is a trade worth making on purpose.
The loop is two lines: roll, then ++counts[value]. Reading that as “add one to
the counter for this face” is the whole idea, and it works because the brackets
take any int expression, not just a literal.
Then a counting loop from 1 to 6 to print it, since you need the face number itself.
Check your answer. The six counts must add up to exactly 1,000. Add a range-for that sums them and prints the total. If it isn’t 1,000, you have a bug, and this is the first program in the book where you couldn’t have spotted it by reading the output.
Stretch: draw it. A number is harder to read than a picture:
1: **************** 167
2: ***************** 173
3: ************** 148One star per ten rolls. std::string bar(counts[face] / 10, '*'); builds it,
the same round-bracket “this many of these” form as the vector, because
std::string supports it too. Integer division from chapter 6 does the
rounding, and here that is exactly what you want.
Stretch two, a loaded die. Change roll(6) so the number 6 comes up twice
as often as it should, then run the tally and confirm the histogram shows it.
Proving a change did what you meant is most of what testing is.