Skip to content

7. Arrays & Vectors

What & why

Most programs work with collections: a list of scores, a row of pixels, lines from a file. C++'s workhorse collection is std::vector — a resizable array that stores its elements contiguously in memory. For a fixed number of elements known at compile time there's std::array. Old C arrays (int a[5]) exist too; you'll recognize them, but prefer the standard types.

std::vector

#include <vector>

std::vector<int> empty;                     // size 0
std::vector<int> scores{90, 72, 85};        // three elements
std::vector<double> zeros(5);               // five 0.0s   (parentheses = count)
std::vector<std::string> names(3, "?");     // three "?"s

std::vector<int> a(3);   // {0, 0, 0}   — size 3
std::vector<int> b{3};   // {3}         — one element! Braces mean "these elements"

Access

scores[0]        // 90 — fast, NO bounds check
scores.at(0)     // 90 — bounds-checked, throws std::out_of_range
scores.front()   // 90
scores.back()    // 85
scores.size()    // 3
scores.empty()   // false

Adding and removing

scores.push_back(100);     // append → {90, 72, 85, 100}
scores.pop_back();         // remove last → {90, 72, 85}
scores.insert(scores.begin() + 1, 60);  // insert at index 1 → {90, 60, 72, 85}
scores.erase(scores.begin());           // remove index 0 → {60, 72, 85}
scores.clear();            // {}

push_back / pop_back at the end are fast. insert / erase in the middle shift every later element — slow for big vectors.

Iterating

for (const auto& s : scores) {
    std::cout << s << ' ';
}

for (std::size_t i = 0; i < scores.size(); ++i) {
    std::cout << i << ": " << scores[i] << '\n';
}

Iterators in one paragraph

v.begin() points at the first element; v.end() points one past the last. Standard algorithms take a range as a pair of iterators [begin, end). You'll learn them properly in the intermediate track; for now, think of them as "positions".

Sorting and searching with <algorithm>

#include <algorithm>
#include <numeric>

std::vector<int> v{5, 2, 9, 1, 7};

std::sort(v.begin(), v.end());                      // {1, 2, 5, 7, 9}
std::sort(v.begin(), v.end(), std::greater<>{});    // {9, 7, 5, 2, 1}
std::reverse(v.begin(), v.end());

auto it = std::find(v.begin(), v.end(), 7);
if (it != v.end()) {
    std::cout << "found 7 at index " << (it - v.begin()) << '\n';
}

int total = std::accumulate(v.begin(), v.end(), 0);   // sum
auto biggest = *std::max_element(v.begin(), v.end());
auto n_even = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });

C++20 ranges let you skip the begin()/end() pairs:

std::ranges::sort(v);
auto it2 = std::ranges::find(v, 7);

(The [](int x) { ... } is a lambda — an inline function. Full coverage in the intermediate track.)

Copying vs. referencing

Assigning a vector copies every element:

std::vector<int> a{1, 2, 3};
std::vector<int> b = a;      // full, independent copy
b[0] = 99;
// a is still {1, 2, 3}

A reference is another name for the same object:

std::vector<int>& r = a;     // no copy
r[0] = 99;
// a is now {99, 2, 3}

This matters most for function parameters (lesson 9): pass vectors as const std::vector<int>& to avoid copying.

std::array: fixed size

#include <array>

std::array<int, 3> rgb{255, 128, 0};
rgb.size();      // 3 — known at compile time
rgb[1] = 64;
std::ranges::sort(rgb);

Same interface as vector minus the resizing. It lives on the stack — no heap allocation.

C-style arrays (for recognition)

int legacy[5] = {1, 2, 3, 4, 5};

They don't know their own size, decay to pointers when passed to functions, and can't be copied with =. You'll meet them in older code and C APIs. Prefer std::array or std::vector.

2D data

std::vector<std::vector<int>> grid(3, std::vector<int>(4, 0));   // 3 rows × 4 cols
grid[1][2] = 7;

for (const auto& row : grid) {
    for (int cell : row) {
        std::cout << cell << ' ';
    }
    std::cout << '\n';
}

Structured bindings for pairs

std::pair holds two values; C++17 structured bindings unpack them:

#include <utility>

std::pair<std::string, int> entry{"Ada", 36};
auto [name, age] = entry;
std::cout << name << " is " << age << '\n';

Common mistakes

  • Out-of-bounds v[i] — undefined behavior, not an exception. Use .at() while learning, and compile with -D_GLIBCXX_DEBUG (GCC) to catch it.
  • std::vector<int> v{5}; expecting five zeros — it's one element, 5. Use v(5).
  • Accidental copies — auto copy = big_vector; or passing by value to functions.
  • Holding a reference/iterator across push_back — the vector may reallocate, leaving it dangling.
  • v.size() - 1 on an empty vector — size() is unsigned, so this is a huge number.
  • Erasing while iterating with a range-for — use std::erase_if(v, pred) (C++20) instead.

Exercises

  1. Read integers until end-of-input into a vector, then print them sorted, the median, and the average.
  2. Remove all negative numbers from a vector using std::erase_if.
  3. Write a loop that reverses a vector in place by swapping elements (std::swap(v[i], v[j])) — don't use std::reverse.
  4. Given std::vector<std::string>, find the longest word.
  5. Create a 10×10 multiplication table in a std::vector<std::vector<int>> and print it aligned with std::format("{:4}", x).
  6. Demonstrate the reallocation trap: take int& first = v[0];, push_back 1000 elements, then print first. Compile with -fsanitize=address and read the report.

Recap

  • std::vector<T> is the default container: contiguous, growable, fast at the end.
  • v[i] is unchecked, v.at(i) is checked. size(), push_back, pop_back, erase.
  • <algorithm> (sort, find, count_if, ...) and <numeric> (accumulate) work on iterator ranges; std::ranges:: versions take the container directly.
  • = copies; & references. Pass containers by const&.
  • std::array<T, N> for fixed-size data; avoid raw C arrays.

Next: Maps & sets →