std::vector in C++: The Dynamic Array You Actually Use

std::vector in C++: The Dynamic Array You Actually Use

If you need a collection of elements in C++, std::vector is usually the right choice. It's a sequence container that stores elements contiguously in memory, like an array, but manages its own size and capacity dynamically.

#include <vector>

std::vector<int> numbers = {1, 2, 3, 4, 5};
numbers.push_back(6);  // Add element to end

The interface is intuitive. The performance is predictable. For most problems, std::vector is the right starting point.

Why Not Just Use Arrays?

C++ has built-in arrays:

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

They're fast and simple, but the size is fixed at compile time. If you need to add a sixth element, you're out of luck. You'd have to allocate a larger array manually, copy the elements over, and manage the memory yourself.

std::vector does this for you. When you add elements beyond its current capacity, it allocates a larger block of memory, moves the existing elements, and frees the old block. This happens automatically.

For stack-allocated arrays with a known, unchanging size, raw arrays or std::array (a fixed-size container from the standard library) are fine. For anything dynamic, std::vector is cleaner and safer.

Size vs Capacity

A vector has two important properties: size and capacity.

Size is the number of elements currently stored. Capacity is the number of elements the vector can hold before it needs to allocate more memory.

std::vector<int> v;
v.reserve(10);  // Set capacity to 10
v.push_back(1);
v.push_back(2);

std::cout << v.size();      // 2
std::cout << v.capacity();  // 10

When you push_back and the size exceeds capacity, the vector reallocates. The exact growth strategy depends on the implementation, but most standard libraries double the capacity each time. This amortizes the cost of reallocation over many insertions.

If you know roughly how many elements you'll need, reserve can avoid multiple reallocations:

std::vector<int> v;
v.reserve(1000);  // Allocate space for 1000 elements upfront
for (int i = 0; i < 1000; ++i) {
    v.push_back(i);  // No reallocation happens
}

This is a minor optimization in most code, but it matters in performance-critical loops.

Common Operations

Adding elements:

v.push_back(10);       // Add to end
v.emplace_back(10);    // Construct in place (more efficient for complex types)
v.insert(v.begin() + 2, 10);  // Insert at index 2

Removing elements:

v.pop_back();          // Remove last element
v.erase(v.begin() + 2); // Remove element at index 2
v.clear();             // Remove all elements

Accessing elements:

int x = v[0];          // No bounds checking
int y = v.at(0);       // Throws exception if out of bounds
int z = v.front();     // First element
int w = v.back();      // Last element

The [] operator is faster but unsafe. at() is safer but has a performance cost. In release builds, use [] when you're certain the index is valid. Use at() when you want runtime safety or during debugging.

Iterating Over a Vector

The range-based for loop is usually the clearest way:

for (int x : v) {
    std::cout << x << " ";
}

If you need to modify elements, use a reference:

for (int& x : v) {
    x *= 2;
}

For more control, use iterators:

for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << " ";
}

Iterators are more flexible and work with standard algorithms like std::sort, std::find, and std::copy.

Performance Characteristics

std::vector provides:

  • O(1) random access: v[i] is as fast as array indexing
  • O(1) amortized insertion at the end: push_back is usually constant time, with occasional reallocations
  • O(n) insertion/removal in the middle: requires shifting elements

Contiguous memory means good cache locality. Iterating over a vector is fast because the CPU can prefetch elements. This often makes std::vector faster than linked structures like std::list, even when theory suggests otherwise.

If you're inserting or removing elements frequently in the middle of the sequence, std::deque or std::list might be better. But profile first. std::vector is often fast enough, and the cache-friendly memory layout compensates for the extra shifts.

When to Use Other Containers

Use std::deque when you need efficient insertion at both ends. Use std::list when you need stable iterators (iterators that don't invalidate on insertion/removal) or frequent splicing. Use std::set or std::map when you need sorted, unique elements or key-value lookups.

But for most problems, std::vector is the right default. It's the most tested, most optimized, and most familiar container in the standard library.

Common Pitfalls

Iterator invalidation: When a vector reallocates, all iterators, pointers, and references to its elements become invalid. If you're holding onto an iterator and the vector grows, you're in undefined behavior territory.

std::vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4);  // May reallocate
// *it is now undefined behavior if reallocation happened

If you need stable references, use std::deque or store indices instead of iterators.

Returning a vector by value: This is safe and efficient in modern C++. Compilers use return value optimization (RVO) or move semantics to avoid unnecessary copies.

std::vector<int> create_vector() {
    std::vector<int> v = {1, 2, 3};
    return v;  // No copy, moved or optimized away
}

Don't return a pointer to a local vector. Return by value.

Multidimensional Vectors

For a 2D array:

std::vector<std::vector<int>> grid(10, std::vector<int>(10, 0));
// 10x10 grid initialized to 0

Each row is a separate vector. This is flexible but not as cache-friendly as a flattened 1D vector. If performance matters, consider storing a single vector and indexing manually:

std::vector<int> grid(10 * 10, 0);
int value = grid[row * 10 + col];

This keeps all elements contiguous in memory.

Further Reading

The C++ reference for std::vector documents every method and property in detail.

For a deep dive into standard library containers and performance considerations, Scott Meyers' Effective STL is still the canonical reference. It's older but the advice holds up.

Bjarne Stroustrup's C++ Core Guidelines recommend std::vector as the default container unless you have a specific reason to choose something else.

If std::vector is your go-to container and you appreciate its balance of simplicity and performance, our std::vector developer tee is a quiet nod to that reliability.

0 comments

Leave a comment

Please note, comments need to be approved before they are published.