Arrays and std::vector

C++ · Essentials · 12 min

std::vector is the most-used container in C++; it is the right default for almost any list of things.

C++ inherited built-in arrays from C: int a[3] = {1, 2, 3};. Their size is fixed at compile time, they don't know their own size when passed to a function (they decay to a pointer), they can't be assigned, and a[10] is not checked. Modern code rarely uses them directly.

std::array<int, 3> (header <array>) is a fixed-size array with no overhead that behaves like a proper value: it knows its size(), can be copied and compared, and works with the standard algorithms.

std::vector<T> (header <vector>) is a dynamic array: elements sit contiguously in heap memory and the vector grows as needed.

  • push_back(x) appends; emplace_back(args...) constructs the element in place; pop_back() removes the last one.
  • size(), empty(), front(), back(), clear().
  • v[i] is unchecked (out of range is undefined behaviour); v.at(i) checks and throws std::out_of_range.
  • reserve(n) preallocates capacity when you know roughly how many elements are coming, avoiding repeated reallocations.

When a vector runs out of capacity it allocates a bigger block (typically 1.5x or 2x), moves the elements over and frees the old block. Appends are therefore cheap on average, but any pointers, references or iterators to elements become invalid after a reallocation. Don't hold on to &v[0] across a push_back.

Contiguous storage is why std::vector is so fast: iteration is cache-friendly and the CPU prefetches well. Even when you insert in the middle now and then, a vector usually beats a linked list. Make it your default container.

Example

#include <array>
#include <iostream>
#include <stdexcept>
#include <vector>

int main() {
    int raw[3] = {1, 2, 3};                // C array: fixed, unchecked
    std::array<int, 3> fixed{4, 5, 6};     // fixed size, knows its size, copyable

    std::vector<int> v;                    // dynamic array on the heap
    v.reserve(10);                         // optional: preallocate
    for (int i = 0; i < 5; ++i) v.push_back(i * i);   // 0 1 4 9 16
    v.pop_back();                          // removes 16

    std::cout << v.size() << ' ' << v.front() << ' ' << v.back() << '\n';  // 4 0 9
    std::cout << v[1] << '\n';             // 1, unchecked access
    try {
        std::cout << v.at(10) << '\n';     // checked
    } catch (const std::out_of_range&) {
        std::cout << "out of range\n";
    }
    std::cout << raw[0] + fixed[2] << ' ' << fixed.size() << '\n';   // 7 3

    std::vector<std::vector<int>> grid(2, std::vector<int>(3, 0));   // 2 x 3 zeros
    grid[1][2] = 7;
}

Remember

  • std::vector is the default container: contiguous, fast, growable
  • v[i] is unchecked, v.at(i) throws on a bad index
  • Growth reallocates and invalidates pointers, references and iterators to elements

Watch out for

  • Keeping a reference or pointer to an element and then calling push_back, which may reallocate and leave it dangling.

Check yourself

  1. A vector has size() == 4 and capacity() == 4. You hold `int& first = v[0];` and call `v.push_back(5);`. What about first?
  2. Which of these knows its size at compile time and can be copied with `=`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/container/vector