Lesson 3 of 9
std::vector is the default container in C++. It's an array that grows.
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {90, 85, 77};
scores.push_back(100);
scores.push_back(68);
std::cout << scores.size() << "\n";
for (int s : scores) {
std::cout << s << " ";
}
std::cout << "\n";
return 0;
}
5
90 85 77 100 68
std::vector<int> — a vector of ints. No size in the type, unlike std::array, because the size
isn't fixed.
Creating one
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> empty;
std::vector<int> listed = {1, 2, 3};
std::vector<int> sized(5); // 5 zeros
std::vector<int> filled(3, 7); // 3 sevens
std::vector<std::string> words = {"evil", "genius"};
std::cout << empty.size() << " " << listed.size() << " "
<< sized.size() << " " << filled.size() << "\n";
std::cout << sized[0] << " " << filled[0] << "\n";
std::cout << words[1] << "\n";
return 0;
}
0 3 5 3
0 7
genius
Watch the brace-versus-parenthesis trap here: std::vector<int> a(5); makes five zeros,
while std::vector<int> a{5}; makes one element containing 5. Braces prefer the
initializer-list constructor. This catches everyone once.
Growing and shrinking
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
std::cout << v.size() << " back=" << v.back() << "\n";
v.pop_back();
std::cout << v.size() << " back=" << v.back() << "\n";
v.insert(v.begin(), 0); // at the front
std::cout << v[0] << v[1] << v[2] << "\n";
v.erase(v.begin());
std::cout << v[0] << "\n";
v.clear();
std::cout << v.size() << " " << std::boolalpha << v.empty() << "\n";
return 0;
}
3 back=3
2 back=2
012
1
0 true
push_back and pop_back work at the end and are cheap. insert and erase at the front
have to shift every remaining element — fine occasionally, a performance problem in a loop
(lesson 13.12).
How growth actually works
A vector holds a contiguous block. When it fills up, it can't just extend — something else may occupy the next bytes. So it allocates a bigger block (typically double), copies everything over, and frees the old one.
That's why size and capacity are different:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
std::cout << "size cap\n";
for (int i = 0; i < 5; ++i) {
v.push_back(i);
std::cout << v.size() << " " << v.capacity() << "\n";
}
return 0;
}
size cap
1 1
2 2
3 4
4 4
5 8
Capacity jumps 1, 2, 4, 8 — doubling. Most push_backs are cheap; occasionally one triggers a
reallocation that copies everything.
If you know roughly how many elements you'll add, reserve up front:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.reserve(1000);
std::cout << v.size() << " " << v.capacity() << "\n";
for (int i = 0; i < 1000; ++i) {
v.push_back(i);
}
std::cout << v.size() << " " << v.capacity() << "\n";
return 0;
}
0 1000
1000 1000
reserve allocates capacity without creating elements, so those 1000 push_backs cause zero
reallocations. Lesson 21.5 measures why this matters.
Reallocation invalidates references
A consequence worth internalising now:
std::vector<int> v = {1, 2, 3};
int& first = v[0];
v.push_back(4); // may reallocate — `first` now dangles
std::cout << first; // undefined behaviour
If the push reallocates, every pointer, reference, and iterator into the vector refers to the freed block. This is a genuinely common bug and Module 11's AddressSanitizer catches it.
Rule: don't hold references into a vector across anything that might resize it.
Access
#include <iostream>
#include <stdexcept>
#include <vector>
int main() {
std::vector<int> v = {10, 20, 30};
std::cout << v[1] << " " << v.at(1) << "\n";
std::cout << v.front() << " " << v.back() << "\n";
try {
std::cout << v.at(99) << "\n";
} catch (const std::out_of_range&) {
std::cout << "out of range\n";
}
return 0;
}
20 20
10 30
out of range
Same rule as std::array: [] is unchecked, .at() throws. And note from lesson 3.3 that
v.size() is unsigned — v.size() - 1 on an empty vector is astronomically large, not −1.
Vectors of anything
#include <iostream>
#include <string>
#include <vector>
struct Task {
std::string title;
bool done;
};
int main() {
std::vector<Task> tasks;
tasks.push_back({"write lesson", true});
tasks.push_back({"verify code", false});
tasks.emplace_back("ship it", false);
for (const Task& t : tasks) {
std::cout << (t.done ? "[x] " : "[ ] ") << t.title << "\n";
}
return 0;
}
[x] write lesson
[ ] verify code
[ ] ship it
emplace_back constructs the element in place from the arguments, rather than building a
temporary and copying it in. For cheap types it makes no difference; for expensive ones it saves
a copy. Prefer it when you're constructing from parts.
That Task struct is a preview of Module 9 — and it's exactly what Milestone Project 1 is built
on.
Try it
Write a function std::vector<int> evens(const std::vector<int>& input) returning only the even
elements, and use reserve sensibly. Test it on {1,2,3,4,5,6,7,8}.
Checkpoint
std::vector<T>is a resizable, contiguous array — the default container in C++.push_back/pop_backat the end are cheap;insert/eraseat the front shift everything.- Size is how many elements exist; capacity is how many fit before reallocating.
- Growth doubles capacity;
reserve(n)avoids repeated reallocation when you know the count. - Reallocation invalidates all references, pointers, and iterators into the vector.
[]is unchecked,.at()throws;.size()is unsigned.emplace_backconstructs in place and avoids a copy.
Questions about this lesson
Log in to ask a question.