Lecture 11+12 — Containers
Source: sc++/ch08.md Duration: 2 x 75 minutes (lectures 11 and 12)
Chapter 8 is split across two lectures:
- Lecture 11 — Array and Vector Basics:
std::array(fixed size),std::vectorconstruction,push_back/pop_back,[]/.at()/.front()/.back(),size/capacity/empty/clear - Lecture 12 — Mutation and Iteration:
insert/erase/reserve/shrink_to_fit, range-basedfor, iterators,.begin()/.end()andauto
Lecture 11 — Array and Vector Basics
Learning Objectives
By the end of lecture 11, students should be able to:
- Declare and use a
std::array<T, N>and explain why size is part of the type - Declare a
std::vector<T>with several construction styles - Add and remove elements at the end with
push_back/pop_back - Access elements with
[],.at(),.front(), and.back() - Distinguish size from capacity and explain why capacity grows in jumps
- Clear and check a vector with
.clear()and.empty()
Materials
- Live coding terminal with
g++(-std=c++23 -Wall -Wextra -pedantic) - A text editor projected for the class
- Copies of
sc++/ch08.mdfor reference
0. Welcome and Review (5 min)
Review multiple choice (from lecture 10): What does this print?
uint8_t a = 250; uint8_t b = 20; uint8_t sum = a + b; std::println("{}", sum);- A. 270
- B. 255
- C. 14
- D. undefined behavior
- E. Ben got this wrong
Answer: C — unsigned wraps.
Today we leave raw C arrays behind and meet the standard library containers
1. The Trouble With C-Style Arrays (5 min)
Review of chapter 2 pain points:
- They do not know their own size
- They decay to pointers when passed to functions (size is lost)
- You cannot return them
- They cannot grow or shrink at runtime
- You pass a separate size parameter everywhere and hope nothing goes out of bounds
The standard library fixes all of this.
2. std::array<T, N> (15 min)
Include <array>. Size is part of the type.
#include <array>
std::array<int, 5> scores = {90, 85, 92, 88, 76};
std::cout << scores[0] << "\n"; // 90 (fast, no bounds check)
std::cout << scores.at(1) << "\n"; // 85 (bounds checked, throws on bad index)
std::cout << scores.size() << "\n"; // 5std::array<int, 5>andstd::array<int, 10>are different types.size()isconstexprand known at compile time
Passing to Functions
void print_scores(const std::array<int, 3>& scores) {
for (size_t i = 0; i < scores.size(); i++) {
std::cout << scores[i] << "\n";
}
}- The function knows the size — no separate parameter required
Trap: The size of a std::array must be a compile-time constant. You cannot use std::array<int, n> where n is a variable. Use std::vector for runtime-sized collections.
3. std::vector<T> — Introduction (8 min)
Include <vector>. Dynamic size, grows as needed.
#include <vector>
std::vector<int> empty; // size 0
std::vector<int> zeros(5); // 5 zeros
std::vector<int> fives(5, 42); // 5 copies of 42
std::vector<std::string> songs =
{"Wannabe", "No Diggity"}; // initializer list- The workhorse container of C++ — use it for almost everything
4. Growing a Vector — push_back / pop_back (10 min)
std::vector<std::string> playlist;
playlist.push_back("Wannabe");
playlist.push_back("No Diggity");
std::cout << playlist.size() << "\n"; // 2
playlist.pop_back();
std::cout << playlist.size() << "\n"; // 1push_backadds one element at the endpop_backremoves the last element (does not return it)
Trap: pop_back() on an empty vector is undefined behavior. Always check .empty() or .size() first.
5. Accessing Elements (7 min)
std::vector<std::string> bands = {"Spice Girls", "Blackstreet", "Oasis"};
std::cout << bands[0] << "\n"; // Spice Girls --- no bounds check
std::cout << bands.at(1) << "\n"; // Blackstreet --- bounds checked
std::cout << bands.front() << "\n"; // Spice Girls
std::cout << bands.back() << "\n"; // Oasis.at()throwsstd::out_of_rangefor bad indices.front()/.back()are convenient shortcuts for the first/last element
6. Size, Capacity, and Empty (15 min)
Two different numbers:
.size()— how many elements the vector holds.capacity()— how much memory it has allocated
std::vector<int> v;
for (int i = 0; i < 5; i++) {
v.push_back(i * 10);
std::cout << "size=" << v.size()
<< " cap=" << v.capacity() << "\n";
}Typical output:
size=1 cap=1
size=2 cap=2
size=3 cap=4
size=4 cap=4
size=5 cap=8- Capacity grows in jumps (typically doubling)
- When capacity is exhausted, the vector reallocates a bigger block and copies everything
push_backis amortized O(1): mostly fast, occasionally does extra work
empty and clear
if (v.empty()) { /* size() == 0 */ }
v.clear(); // size -> 0Wut: After v.clear(), v.size() is 0 but v.capacity() is unchanged. The memory is kept for future use.
7. Try It — Live Demo (7 min)
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v;
for (int i = 0; i < 8; i++) {
v.push_back(i);
std::cout << "size=" << v.size()
<< " cap=" << v.capacity() << "\n";
}
}Have the class predict when the next reallocation happens.
8. Wrap-up Quiz (3 min)
Q1. What does this print?
std::vector<int> v = {10, 20, 30};
v.push_back(40);
v.pop_back();
v.pop_back();
std::cout << v.size() << " " << v.back() << "\n";A. 3 30 B. 2 20 C. 2 30 D. 4 30 E. Ben got this wrong
Answer: B
Q2. What does this print?
std::vector<int> v = {1, 2, 3};
v.clear();
std::cout << v.size() << " " << v.empty() << "\n";A. 0 0 B. 0 1 C. 3 0 D. 3 1 E. Ben got this wrong
Answer: B — .empty() returns true (which prints as 1).
9. Assignment / Reading (5 min)
- Read: chapter 8, remaining sections — insert/erase/reserve, range-based for with references, iterators and
auto - Do: chapter 8 exercises 4, 6, 7, 9, 11, 12 (iteration, insert/erase/reserve, auto references)
- Bring: questions about capacity growth if anything was surprising
Key Points to Reinforce
std::arrayis compile-time sized;std::vectoris runtime-sized[]is fast,.at()is safe- Vector capacity grows in jumps;
.clear()keeps the memory - Never
pop_backan empty vector
Lecture 12 — Mutation and Iteration
Learning Objectives
By the end of lecture 12, students should be able to:
- Use
insertanderaseto modify a vector at any position - Preallocate with
reserveand shrink withshrink_to_fit - Iterate with a range-based
forloop usingconst auto&orauto&as appropriate - Use
.begin()/.end()iterators and dereference with*it - Recognize iterator invalidation after insert/erase
Materials
- Live coding terminal with
g++(-std=c++23 -Wall -Wextra -pedantic) - A text editor projected for the class
- Copies of
sc++/ch08.mdfor reference
0. Welcome and Review (5 min)
Review multiple choice (from lecture 11): A vector has size 3 and capacity 8. How many more
push_backcalls before it reallocates?- A. 3
- B. 5
- C. 7
- D. 8
- E. Ben got this wrong
Answer: B — capacity 8 minus current size 3 = 5 more elements fit without reallocation.
Today we finish chapter 8 with mutation in the middle and iteration
1. Insert and Erase (15 min)
std::vector<std::string> lista = {"Creep", "No Rain", "Linger"};
lista.insert(lista.begin() + 1, "Possum Kingdom");
// {"Creep", "Possum Kingdom", "No Rain", "Linger"}
lista.erase(lista.begin());
// {"Possum Kingdom", "No Rain", "Linger"}- Position is expressed as an iterator —
lista.begin() + nmeans “index n” insertplaces the new element before the given positioneraseremoves one element (or a range)
Why They Are Slower
- Inserting in the middle shifts every subsequent element over
- Erasing shifts every subsequent element back
- Both are O(n) in the worst case — prefer
push_back/pop_backwhen you can
Trap: Insert and erase invalidate iterators, pointers, and references into the vector. After either call, get fresh iterators before touching the vector again.
2. Reserve and shrink_to_fit (10 min)
std::vector<int> v;
v.reserve(1000); // allocate space for 1000 ints
// v.size() == 0, v.capacity() >= 1000reserve(n)preallocates capacity so subsequentpush_backs do not reallocate- Use it when you know how many elements you will add
v.shrink_to_fit(); // non-binding request to match capacity to sizeshrink_to_fitasks the implementation to release excess memory- It is non-binding — the implementation may ignore it
3. Range-Based for Loop (15 min)
std::vector<std::string> songs = {"Wannabe", "No Diggity"};
for (const auto& song : songs) {
std::cout << song << "\n";
}Breakdown:
auto— let the compiler deduce the type&— reference, no copyconst— promise not to modify
Modifying Elements
std::vector<int> values = {1, 2, 3, 4, 5};
for (auto& v : values) {
v *= 10;
}
// {10, 20, 30, 40, 50}Tip: Prefer const auto& when reading. Use auto& when modifying. Avoid plain auto (no &) for anything larger than a primitive — it copies every element.
4. Iterators (15 min)
Under the hood, range-based for uses iterators.
std::vector<std::string> canciones = {"Wannabe", "No Diggity"};
for (auto it = canciones.begin(); it != canciones.end(); ++it) {
std::cout << *it << "\n";
}.begin()returns an iterator to the first element.end()returns an iterator one past the last element — not at the last element*itdereferences (like a pointer) to get the element++itadvances to the next element
Wut: .end() points past the last element, not at it. The valid range is [begin, end) — a half-open interval. It makes loops cleaner and avoids off-by-one errors.
auto With Iterators
// without auto
std::vector<std::string>::iterator it = canciones.begin();
// with auto
auto it = canciones.begin();autoshines here — iterator types are long and the type is obvious from context
Why Use Iterators Directly?
- Standard library algorithms require them (sort, find, copy, …)
- You can iterate backward (
rbegin/rend) - You can erase while iterating safely
5. Try It — Live Demo (6 min)
#include <iostream>
#include <vector>
int main()
{
std::vector<int> nums = {1, 2, 3, 4, 5};
for (auto& n : nums) {
n *= 2;
}
for (const auto& n : nums) {
std::cout << n << " ";
}
std::cout << "\n";
}Walk through how the range-based form rewrites to an explicit iterator loop.
6. Wrap-up Quiz (4 min)
Q1. What does this print?
std::vector<int> v = {10, 20, 30, 40, 50};
v.insert(v.begin() + 2, 25);
v.erase(v.begin());
for (const auto& n : v) { std::cout << n << " "; }
std::cout << "\n";A. 10 20 25 30 40 50 B. 20 25 30 40 50 C. 20 25 30 40 D. 10 25 30 40 50 E. Ben got this wrong
Answer: B
Q2. Where is the bug?
std::vector<int> scores = {95, 87, 91};
for (int i = 0; i <= scores.size(); i++) {
std::cout << scores[i] << "\n";
}A. scores cannot be iterated B. [] does not work on std::vector C. <= should be < — reads one past the end D. i should be size_t E. Both C and D — Ben got this wrong
Answer: E — the <= causes an off-by-one read past the last element, and int vs size_t is a signed/unsigned mismatch.
Q3. Why is for (auto x : vec) usually wrong for std::vector<std::string>?
A. It does not compile B. It copies every string on each iteration C. It modifies the original vector D. It only works for vectors of ints E. Ben got this wrong
Answer: B
7. Assignment / Reading (5 min)
- Read: chapter 9 of Gorgo Starting C++ (I/O streams — string streams, file streams, stream manipulators)
- Do: all 9 exercises at the end of chapter 9
- Bring: a plain-text file with 3-5 lines for next lecture’s file-reading demo
Key Points to Reinforce
insert/eraseshift elements; prefer end operations when you can- Insert/erase invalidate iterators — get fresh ones afterward
- Range-based
forwithconst auto&is the default for read-only loops .end()is one past the last element — half-open interval[begin, end)autosaves you from writingstd::vector<T>::iteratorby hand