Lists with std::vector
A list that grows while the program runs: adding, reading, replacing and walking it with a range-based for that cannot run off the end.
A list that grows
A std::vector is a list of values that all have the same type, and it can change size as the program runs. You write the element type in the angle brackets: std::vector<std::string> is a list of text, std::vector<int> is a list of whole numbers.
You can build one from a list of values in braces, std::vector<int> scores = {4, 7, 2};, or start it empty and use push_back to add to the end. pop_back removes the last item. Together those four operations are most of what you do with a list.
Reading what is in it
size() is the count. Square brackets reach one position, counting from zero, so the last valid position is size minus one. front() and back() read the ends without arithmetic, and at(position) is the version that checks — it stops the program with an error rather than reading whatever happens to be there.
Square brackets are the fast, unchecked reach. On a list you built yourself in the same function they are perfectly safe, and they are what the examples use; reach for at when a position came from somewhere you do not control.
Walking every item
The range-based loop is the tool for looking at a list: for (const std::string& title : books). Read it as "for each title in books". The const says you will not change the items, and the ampersand says do not copy each one — the loop hands you a reference to the item already in the list.
Dropping the ampersand still works, and quietly copies every element on every turn. On a list of short numbers you would never notice; on a list of long strings it is the difference between a fast loop and a slow one.
Passing a vector to a function
A vector passed by value is copied, items and all, which is expensive and rarely what you meant. Pass it by reference — void show(const std::vector<int>& scores) — and the function reads the caller list without copying it. Drop the const when the function is supposed to add or change items, and keep the ampersand.
Returning a vector is fine: the compiler moves it out rather than copying it. That is why a function that builds a list can return one by value without the cost you might expect from the parameter side.
A list of lists
A vector's element type can be another vector: std::vector<std::vector<int>> grid; is a grid whose rows may have different lengths. grid[1][2] reaches the third item of the second row, and grid.size() is the number of rows rather than the number of numbers.
Two nested loops over it read like the table they represent, and the same boundary care applies at both levels: the outer index must be less than the number of rows, and the inner one less than the length of the row you are on.
A list that grows
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> books = {"Dune", "It"};
books.push_back("Emma");
std::cout << "Count: " << books.size() << '\n';
std::cout << "First: " << books[0] << '\n';
std::cout << "Last: " << books.back() << '\n';
for (const std::string& title : books) {
std::cout << "- " << title << '\n';
}
std::vector<int> numbers = {4, 7, 2};
numbers[1] = 9;
int total = 0;
for (int n : numbers) {
total += n;
}
std::cout << "Total: " << total << '\n';
return 0;
}A reading list that grows
Run it, then add two more books with push_back and watch the count change. Replace the numeric total with a loop that prints each number doubled, and use back() to print the last one.
A vector keeps its items next to each other in order, and the position numbers never move on their own. That is why a list is often easier to reason about than a map.
Size is not capacity
A vector keeps its items in one block of memory. When push_back runs out of room, it allocates a larger block, moves the items across, and frees the old one. size() is how many items you hold; capacity() is how many fit before the next move. The growth is geometric, so adding n items one at a time is fast on average rather than quadratic.
That move matters because it invalidates things that pointed into the old block: references, pointers and iterators. Holding int& first = numbers[0]; and then pushing enough items can leave first naming freed memory. Index again after the push, or reserve the space first when you know the final count.
Erasing from the middle shifts everything after it one place to the left, so the size drops by one and the positions change. That is O(n) work, not a constant-time unlink, and it is why a list you remove from constantly may be the wrong container.
Three vector traps: reading numbers[numbers.size()], which is one past the end; calling back() or pop_back() on an empty vector, which is undefined behaviour; and keeping a reference across a push_back that reallocates. at() checks and throws, while square brackets do not.
Try these next
- Add five numbers with
push_back, then print the sum and the average. - Remove the middle item with
eraseand print the list before and after. - Print
size()andcapacity()after eachpush_backand watch the capacity jump.
A score list: add, update, erase
Run it and follow the count as the list changes. Then add a sixth score, print the new average, and erase the first item instead of the third.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does push_back do to a vector?
