std::vector is the most-used container in the C++ standard library. It is a dynamic array: it stores elements in a single contiguous block of heap memory, supports random access in O(1), and grows automatically when new elements are pushed in. Understanding how it works internally — and especially how growth, reallocation, and iterator invalidation operate — is essential for writing correct and efficient C++ code.
Before We Begin — The Simplest Possible Example
If you have never used std::vector before, start right here. You only need one header.
#include<vector>#include<iostream>int main() {
// 1. Create an empty vector of integers
std::vector<int> numbers;
// 2. Add elements one by one to the back
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
// 3. Access elements by index — exactly like a plain array
std::cout << numbers[0] << "\n"; // 10
std::cout << numbers[1] << "\n"; // 20
std::cout << numbers[2] << "\n"; // 30// 4. Loop over all elementsfor (int n : numbers) {
std::cout << n << " ";
}
std::cout << "\n"; // Output: 10 20 30// 5. Ask the vector how many elements it holds
std::cout << numbers.size() << "\n"; // 3// 6. Remove the last element
numbers.pop_back();
std::cout << numbers.size() << "\n"; // 2// 7. You can also create a vector with initial values directly
std::vector<int> scores = {95, 87, 72, 100};
std::cout << scores[3] << "\n"; // 100
}
A std::vector behaves just like an array for reading and writing elements (v[i]), but it can grow automatically as you add elements. You never have to worry about pre-allocating the right size. The rest of this page explains how it does that and how to use it efficiently.
std::vector<T> is a contiguous, dynamically resizable array. Its elements are always stored next to each other in memory (like a C array), but unlike a C array its size can change at runtime. The vector object itself is small and lives on the stack (or wherever you declare it); the actual elements live on the heap.
Key guarantees from the standard:
Elements are stored in contiguous memory — &v[0] is a valid C-style array pointer.
Random access (v[i]) is O(1).
push_back is amortised O(1).
Insertion and deletion in the middle is O(n) — elements must be shifted.
2. Memory Model — Internal Structure
A std::vector object holds exactly three things:
data — a pointer to the heap-allocated element array.
size — the number of elements currently stored.
capacity — the total number of elements the current allocation can hold before a reallocation is needed.
On a 64-bit system each of those is 8 bytes, so sizeof(std::vector<T>) == 24 bytes regardless of how many elements it contains.
Because elements are contiguous, the compiler can vectorise loops over a std::vector exactly as it would over a C array. The data pointer is the only indirection: once dereferenced, the CPU accesses a flat array and cache prefetchers work perfectly.
What the vector object looks like in code
// Conceptual equivalent of what the standard library stores internally:template <typename T>
class vector {
T* data_; // pointer to heap array
size_t size_; // number of live elements
size_t capacity_; // allocated slots// ... allocator + methods
};
// On a 64-bit system:
std::vector<int> v;
std::cout << sizeof(v) << "\n"; // prints 24// The elements live on the heap, not inside 'v':
v.push_back(10);
int* raw = v.data(); // pointer to the heap block
3. Size vs Capacity and Growth Strategy
size is how many elements are currently in the vector. capacity is how many elements fit in the current allocation before a new one is needed. capacity >= size always holds.
When push_back is called and size == capacity, the vector must reallocate:
Allocate a new, larger heap block (typically 2× the old capacity, but the factor is implementation-defined).
Move (or copy) every existing element into the new block.
Destroy elements in and deallocate the old block.
Update the internal pointer, size, and capacity.
Why doubling gives amortised O(1)
Suppose we start with capacity 1 and push n elements. Reallocations happen at sizes 1, 2, 4, 8, …, n. The total copy work across all reallocations is 1 + 2 + 4 + … + n = 2n − 1, so the average cost per push_back is constant — O(1) amortised.
#include<vector>#include<string>// 1. Default — empty vector, no allocation yet
std::vector<int> a;
// 2. Fill — 5 elements, all zero-initialised
std::vector<int> b(5); // {0, 0, 0, 0, 0}// 3. Fill with value
std::vector<int> c(5, 42); // {42, 42, 42, 42, 42}// 4. Initialiser list
std::vector<int> d = {1, 2, 3, 4};
// 5. Copy construction — deep copy
std::vector<int> e = d; // new heap allocation, elements copied// 6. Move construction — steals the heap buffer
std::vector<int> f = std::move(d); // d becomes empty, no copy// 7. Range construction — from any iteratorsint arr[] = {10, 20, 30};
std::vector<int> g(arr, arr + 3);
// 8. Works with any element type
std::vector<std::string> names = {"Alice", "Bob", "Carol"};
5. Core Operations and Their Complexity
Operation
Method
Time complexity
Notes
Random access
v[i] / v.at(i)
O(1)
at() throws on out-of-range; [] does not
First element
v.front()
O(1)
UB if empty
Last element
v.back()
O(1)
UB if empty
Append at back
v.push_back(x)
O(1) amortised
May reallocate
Construct at back
v.emplace_back(args)
O(1) amortised
Constructs in-place, avoids copy
Remove last
v.pop_back()
O(1)
Does not reallocate
Insert at position
v.insert(it, x)
O(n)
Shifts all following elements right
Erase at position
v.erase(it)
O(n)
Shifts all following elements left
Erase range
v.erase(first, last)
O(n)
Shifts suffix
Clear all elements
v.clear()
O(n)
Destroys elements; keeps capacity
Number of elements
v.size()
O(1)
Current capacity
v.capacity()
O(1)
Empty check
v.empty()
O(1)
Raw pointer to data
v.data()
O(1)
Valid C-array pointer
Shrink allocation
v.shrink_to_fit()
O(n)
Non-binding hint to release excess capacity
push_back vs emplace_back
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
std::vector<Point> pts;
// push_back: constructs a temporary Point, then moves it into the vector
pts.push_back(Point(1, 2));
// emplace_back: forwards arguments directly to the constructor inside the vector// NO temporary — the object is constructed in-place in the reserved slot
pts.emplace_back(3, 4);
Prefer emplace_back when constructing complex objects. For cheap-to-copy primitives like int or double the difference is negligible and the compiler will optimise both identically.
insert and erase — with diagram
std::vector<int> v = {1, 2, 3, 4, 5};
// Insert 99 before index 2: {1, 2, 99, 3, 4, 5}
v.insert(v.begin() + 2, 99);
// Elements at [2], [3], [4] are shifted right — O(n)// Erase element at index 1: {1, 99, 3, 4, 5}
v.erase(v.begin() + 1);
// Elements at [2], [3], [4] are shifted left — O(n)// Erase range [1, 3): {1, 4, 5}
v.erase(v.begin() + 1, v.begin() + 3);
6. reserve() vs resize()
These two methods look similar but do very different things:
reserve(n) — ensures the vector can hold at least n elements without reallocation. It changes capacity only; size is unchanged. No elements are constructed.
resize(n) — changes size to n. If n > size, new elements are value-initialised (zero for ints). If n < size, excess elements are destroyed.
std::vector<int> v;
// reserve — capacity grows, size stays 0
v.reserve(100);
std::cout << v.size() << '\n'; // 0
std::cout << v.capacity() << '\n'; // >= 100// Now 100 push_backs happen without any reallocationfor (int i = 0; i < 100; ++i) v.push_back(i);
// resize — size grows, elements initialised to 0
std::vector<int> w;
w.resize(5); // {0, 0, 0, 0, 0}
w.resize(8, 99); // {0, 0, 0, 0, 0, 99, 99, 99}
w.resize(3); // {0, 0, 0} — elements destroyed
Performance pattern: if you know the final size of the vector up front, call reserve(n) before filling it. This eliminates all reallocations and the associated memory copies, halving the constant factor for build-and-fill workloads.
7. Iterators and Iterator Invalidation
Iterating over a vector
std::vector<int> v = {10, 20, 30, 40};
// Range-for (most idiomatic)for (const auto& x : v) std::cout << x << ' ';
// Iterator-basedfor (auto it = v.begin(); it != v.end(); ++it)
std::cout << *it << ' ';
// Index-basedfor (size_t i = 0; i < v.size(); ++i)
std::cout << v[i] << ' ';
// Reversefor (auto it = v.rbegin(); it != v.rend(); ++it)
std::cout << *it << ' ';
// STL algorithm
std::for_each(v.begin(), v.end(), [](int x){ std::cout << x << ' '; });
Iterator invalidation rules
Iterator invalidation is one of the most common sources of bugs with std::vector. An iterator (or pointer, or reference) to a vector element becomes invalid — it may point to garbage or a deallocated address — after certain operations.
Operation
Iterators / pointers / references invalidated
push_back / emplace_back (causes reallocation)
All iterators, pointers, and references to all elements
push_back / emplace_back (no reallocation)
None — end iterator is invalidated but element iterators stay valid
insert (causes reallocation)
All
insert (no reallocation)
Iterator to insertion point and everything after it
erase
Iterator to erased element and everything after it
pop_back
Iterator to the removed last element and end()
clear
All element iterators
resize / reserve (if reallocation)
All
Index access, front(), back()
None
// WRONG — iterator used after push_back may have reallocated
std::vector<int> v = {1, 2, 3};
auto it = v.begin(); // iterator to element 1
v.push_back(4); // may reallocate
std::cout << *it; // UNDEFINED BEHAVIOUR — it may be dangling// CORRECT — re-obtain iterator after modifying the vector
v.push_back(5);
it = v.begin(); // refresh iterator
std::cout << *it; // safe: 1// CORRECT — use index instead of iterator when mutating
size_t idx = 0;
v.push_back(6);
std::cout << v[idx]; // safe: v[0] is always 1
8. 2D Vectors
A vector<vector<T>> is a vector of vectors. Each inner vector is an independent heap allocation — the rows are NOT contiguous with each other in memory.
// Declare a 3×4 grid of zerosint rows = 3, cols = 4;
std::vector<std::vector<int>> grid(rows, std::vector<int>(cols, 0));
// Access and modify
grid[1][2] = 42;
// Iteratefor (const auto& row : grid) {
for (int val : row) std::cout << val << ' ';
std::cout << '\n';
}
// Jagged (irregular) — each row can have different length
std::vector<std::vector<int>> jagged;
jagged.push_back({1});
jagged.push_back({2, 3});
jagged.push_back({4, 5, 6});
For a dense matrix where all rows have the same length, a flat 1D vector is much better for cache performance. Access element [r][c] as v[r * cols + c].
// Cache-friendly flat matrix — single contiguous allocationint rows = 3, cols = 4;
std::vector<int> matrix(rows * cols, 0);
// Access element at (r, c)
matrix[1 * cols + 2] = 42; // row 1, col 2// Iterate all elements — perfect for CPU prefetcherfor (int r = 0; r < rows; ++r)
for (int c = 0; c < cols; ++c)
std::cout << matrix[r * cols + c] << ' ';
9. Move Semantics with Vectors
Move semantics (std::move) let you transfer ownership of a vector's heap buffer to another vector in O(1) — no element copying.
std::vector<int> a = {1, 2, 3, 4, 5};
// Move construction — steals a's buffer; a becomes empty
std::vector<int> b = std::move(a);
// b: {1,2,3,4,5} a: {} (a.data() == nullptr, a.size() == 0)// Move assignment — same idea
std::vector<int> c;
c = std::move(b);
// c: {1,2,3,4,5} b: {}// Return value optimisation (RVO/NRVO) — compiler elides the copy/move entirely
std::vector<int> make_vec(int n) {
std::vector<int> v(n);
std::iota(v.begin(), v.end(), 0);
return v; // No copy; compiler constructs directly in the caller's variable
}
auto v = make_vec(1000); // zero copies
Passing vectors to functions:
const std::vector<T>& — read-only access, no copy. Use for most function parameters.
std::vector<T>& — modifiable reference to the caller's vector.
std::vector<T> (by value) — makes a copy. Use when the function needs its own independent copy or you pass with std::move.
std::vector<T>&& (rvalue ref) — explicit move parameter for sink functions.
10. std::vector vs Raw Array vs std::array
Property
Raw array int[N]
std::array<int,N>
std::vector<int>
Size known at compile time
Yes (fixed)
Yes (fixed)
No (runtime)
Memory location
Stack (or static)
Stack (or static)
Object on stack, elements on heap
Dynamic resize
No
No
Yes
Contiguous elements
Yes
Yes
Yes
Bounds checking
No
at() yes, [] no
at() yes, [] no
Copy / assign
No (decays to pointer)
Yes (value semantics)
Yes (value semantics)
STL algorithms compatible
Via std::begin/end
Yes
Yes
Overhead over raw array
None
None
3 pointer-sized fields + heap alloc
Best use case
Low-level C interop
Fixed-size, stack, constexpr
General purpose, size unknown at compile time
11. Common Pitfalls
Pitfall 1 — Out-of-bounds access with []
std::vector<int> v = {1, 2, 3};
v[5]; // UNDEFINED BEHAVIOUR — no bounds check
v.at(5); // throws std::out_of_range — safe for debugging
Pitfall 2 — Storing a pointer/reference and then modifying the vector
std::vector<int> v = {1, 2, 3};
int& ref = v[0]; // reference to first element
v.push_back(4); // may reallocate
std::cout << ref; // UNDEFINED BEHAVIOUR — ref is dangling
Pitfall 3 — Not reserving when size is known
// Slow — N reallocations for N pushes from empty
std::vector<int> v;
for (int i = 0; i < N; ++i) v.push_back(i);
// Fast — zero reallocations
std::vector<int> v;
v.reserve(N);
for (int i = 0; i < N; ++i) v.push_back(i);
Pitfall 4 — Erasing inside a range-for loop
// WRONG — erasing while iterating with range-for is undefinedfor (const auto& x : v)
if (x == 3) v.erase(...); // iterator invalidated mid-loop// CORRECT — erase-remove idiom#include<algorithm>
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// CORRECT — erase_if (C++20)
std::erase_if(v, [](int x){ return x == 3; });
Pitfall 5 — vector<bool> is not a regular vector
// std::vector<bool> is a special case — it packs bits (1 bit per bool)
// This means: operator[] does NOT return a bool& — it returns a proxy object
// You cannot take the address of an element, and auto& may not work as expected
std::vector<bool> flags = {true, false, true};
auto f = flags[0]; // f is a proxy, not bool&bool* p = &flags[0]; // COMPILE ERROR — cannot take address of proxy// If you need a real array of bools, use:
std::vector<uint8_t> flags2 = {1, 0, 1}; // or std::deque<bool>
std::vector<bool> is a well-known design mistake in the standard library. It specialises vector to store bits (1 bit per element instead of 1 byte), which violates the usual container semantics. Unless you specifically need the space optimisation, avoid it.
Practice Tasks
Solve each problem on your own before revealing the solution. All you need is #include <vector> and #include <iostream>.
1Create and inspect a vector.
Create a vector containing the values {3, 7, 1, 4, 9}. Print every element on one line separated by spaces, then print the total number of elements.
#include<vector>#include<iostream>int main() {
std::vector<int> v = {3, 7, 1, 4, 9};
for (int x : v) std::cout << x << " ";
std::cout << "\n"; // 3 7 1 4 9
std::cout << v.size() << "\n"; // 5
}
2Sum of all elements.
Write a function int sum(const std::vector<int>& v) that returns the sum of all elements. Test it with {1, 2, 3, 4, 5} — the answer should be 15.
#include<vector>#include<iostream>intsum(const std::vector<int>& v) {
int total = 0;
for (int x : v) total += x;
return total;
}
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::cout << sum(v) << "\n"; // 15
}
3Find the maximum value.
Without using any standard library algorithm, write a function that finds and returns the largest value in a non-empty vector. Test with {4, 2, 8, 1, 9, 3} — answer: 9.
#include<vector>#include<iostream>intmax_val(const std::vector<int>& v) {
int best = v[0]; // start with first elementfor (size_t i = 1; i < v.size(); ++i) {
if (v[i] > best) best = v[i];
}
return best;
}
int main() {
std::vector<int> v = {4, 2, 8, 1, 9, 3};
std::cout << max_val(v) << "\n"; // 9
}
4Remove all even numbers.
Start with {1, 2, 3, 4, 5, 6, 7, 8}. Remove every even number using the erase-remove idiom. The result should be {1, 3, 5, 7}.
#include<vector>#include<algorithm>#include<iostream>int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
// std::remove shifts non-even elements to the front and returns// an iterator to the new logical end. erase then deletes the rest.
v.erase(
std::remove_if(v.begin(), v.end(), [](int x){ return x % 2 == 0; }),
v.end()
);
for (int x : v) std::cout << x << " "; // 1 3 5 7
}
5Reverse a vector in-place.
Reverse the elements of {1, 2, 3, 4, 5} without using std::reverse. Result: {5, 4, 3, 2, 1}. Hint: swap the first element with the last, then the second with the second-to-last, and so on.
#include<vector>#include<iostream>#include<utility>// std::swapvoidreverse_vec(std::vector<int>& v) {
size_t left = 0;
size_t right = v.size() - 1;
while (left < right) {
std::swap(v[left], v[right]);
++left;
--right;
}
}
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
reverse_vec(v);
for (int x : v) std::cout << x << " "; // 5 4 3 2 1
}