Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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_back at the end are cheap; insert/erase at 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_back constructs in place and avoids a copy.

Questions about this lesson

Log in to ask a question.