std::vector
Dynamic contiguous array container. Default choice for sequential collections. O(1) random access and amortized O(1) append. Reallocates when capacity exhausted.
std::vectorsince C++98A dynamically-resizable, contiguous array stored on the heap that manages its own memory and provides O(1) random access and amortized O(1) append operations.
Overview
std::vector<T> is the most widely-used container in C++. It owns a heap-allocated buffer of contiguous elements, automatically resizing when capacity is exceeded. Every element is accessible in constant time by index, and appending to the end (when spare capacity exists) is also constant time.
Choose std::vector by default for any sequential collection unless you have a specific reason otherwise (e.g., front insertion performance, pointer stability). It dominates because:
- Cache locality: Elements are contiguous; hardware prefetchers are optimized for this layout.
- Simplicity: Familiar to all C++ programmers; lowest cognitive overhead.
- Performance: Wins against
std::list,std::deque, and other alternatives in nearly all real-world workloads (even front insertion onlistis rarely worth the cache penalty for reasonable sizes).
The key trade-off is memory overhead: std::vector always allocates more than the current size (exponentially, typically doubling). Also, reallocations invalidate iterators, pointers, and references to elements—a subtle hazard in large codebases.
Syntax
#include <vector>
// Basic declarations (C++98)
std::vector<int> v; // empty
std::vector<int> v(10); // 10 default-initialized ints
std::vector<int> v(10, 42); // 10 ints, each set to 42
// Initializer list (C++11)
std::vector<int> v = {1, 2, 3, 4, 5};
// Class template argument deduction (C++17)
std::vector v{1, 2, 3}; // deduces vector<int>
// Template signature
template<typename T, typename Alloc = std::allocator<T>>
class vector { /* ... */ };Key member functions
| Function | Complexity | Introduced | Notes |
|---|---|---|---|
operator[i] | O(1) | C++98 | No bounds check; UB if out of range |
at(i) | O(1) | C++98 | Throws std::out_of_range |
front(), back() | O(1) | C++98 | UB if empty |
data() | O(1) | C++11 | Raw pointer to underlying buffer |
push_back(x) | Amortized O(1) | C++98 | May reallocate and invalidate all iterators |
emplace_back(args…) | Amortized O(1) | C++11 | Constructs element in-place; no temporary |
pop_back() | O(1) | C++98 | UB if empty; does not shrink capacity |
insert(it, x) | O(n) | C++98 | Invalidates all iterators at or after insertion point |
erase(it) | O(n) | C++98 | Shifts elements; invalidates from erase point onward |
resize(n) | O(n) | C++98 | Grows or shrinks; value-initializes new elements |
reserve(n) | O(n) worst-case | C++98 | Pre-allocates without changing size |
clear() | O(n) | C++98 | Destroys all elements; capacity unchanged |
shrink_to_fit() | O(n) | C++11 | Non-binding request to reduce capacity to size() |
Examples
Basic usage and capacity management
#include <vector>
#include <iostream>
int main() {
std::vector<int> v;
std::cout << "Initial: size=" << v.size()
<< ", capacity=" << v.capacity() << '\n'; // 0, 0
v.push_back(10);
v.push_back(20);
std::cout << "After 2 pushes: size=" << v.size()
<< ", capacity=" << v.capacity() << '\n'; // 2, likely 2 or 4
v.reserve(100); // Guarantee space for 100 elements without reallocation
std::cout << "After reserve(100): capacity=" << v.capacity() << '\n'; // 100
// Access
std::cout << v[0] << '\n'; // 10 — unchecked, fast
std::cout << v.at(0) << '\n'; // 10 — throws if out of range
}Avoiding reallocations with reserve()
#include <vector>
// Inefficient: multiple reallocations
std::vector<int> inefficient() {
std::vector<int> v;
for (int i = 0; i < 100'000; ++i)
v.push_back(i); // Reallocates when size reaches capacity
return v;
}
// Efficient: single allocation
std::vector<int> efficient() {
std::vector<int> v;
v.reserve(100'000); // Single allocation up-front
for (int i = 0; i < 100'000; ++i)
v.push_back(i); // No reallocations
return v;
}Range-based for and iterators
std::vector<std::string> names{"Alice", "Bob", "Carol"};
// C++11: range-for loop
for (const auto& name : names)
std::cout << name << '\n';
// Manual iteration
for (auto it = names.begin(); it != names.end(); ++it)
std::cout << *it << '\n';
// Reverse iteration
for (auto it = names.rbegin(); it != names.rend(); ++it)
std::cout << *it << '\n';Insertion and erasure
std::vector<int> v{1, 2, 3, 4, 5};
// Insert at position (O(n) shift)
auto it = v.begin() + 2;
v.insert(it, 99); // {1, 2, 99, 3, 4, 5}
// Erase single element (O(n) shift)
v.erase(v.begin() + 2); // {1, 2, 3, 4, 5}
// Erase by value (erase-remove idiom, pre-C++20)
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// C++20: std::erase and std::erase_if (cleaner, no need for <algorithm> in same way)
std::erase(v, 3); // remove all 3s
std::erase_if(v, [](int x) { return x % 2 == 0; }); // remove evensEmplacement construction (C++11+)
struct Point { int x, y; };
std::vector<Point> pts;
// Traditional: construct temporary, then move into vector
pts.push_back(Point{1, 2});
// C++11+: construct directly in vector's storage
pts.emplace_back(3, 4); // Arguments forwarded; no temporary object createdBest Practices
1. Reserve capacity upfront if size is known
Reallocations during growth are expensive and invalidate iterators. If the final size is known or bounded, reserve in advance:
std::vector<int> v;
v.reserve(1000); // Allocate once
for (int i = 0; i < 1000; ++i)
v.push_back(compute(i)); // No reallocations2. Prefer emplace_back() over push_back()
emplace_back() (C++11) constructs elements in-place without temporary objects. For large or complex types, this is noticeably faster.
3. Use at() defensively, [] in proven code
at() checks bounds and throws std::out_of_range; [] is unchecked. Use at() when bounds are uncertain (e.g., user input); use [] in tight loops where bounds are mathematically proven.
4. Pass by const reference, not by value
Passing a vector by value copies the entire buffer. Always pass by const reference for read-only parameters:
void process(const std::vector<int>& v); // Efficient
void process(std::vector<int> v); // Expensive copy — avoid
void process(std::vector<int>&& v); // Take ownership (move)5. Avoid storing iterators across modifications
Do not hold iterators from begin() or end() if the vector will be resized. Use indices, or restructure to eliminate the iterator:
std::vector<int> v{1, 2, 3, 4, 5};
auto it = v.begin() + 2;
v.push_back(6); // May reallocate; it is now dangling
// std::cout << *it; // UB
// SAFE: Use indices or guarantee no reallocation
int idx = 2;
v.reserve(10); // Prevent reallocation
v.push_back(6); // Safe now
std::cout << v[idx];Common Pitfalls
Iterator invalidation on reallocation
When a vector reallocates (because size() == capacity() and you append), every iterator, pointer, and reference to elements becomes invalid. This is the #1 source of subtle bugs:
std::vector<int> v{1, 2, 3};
int* ptr = &v[0];
v.push_back(4); // May reallocate
std::cout << *ptr; // Dangling pointer — undefined behaviorstd::vector<bool> is not a real container
std::vector<bool> is a specialized, bit-packed version that breaks container semantics. Iterators no longer return bool&, and operator& doesn't yield a valid pointer. Avoid it:
std::vector<bool> vb{true, false, true}; // Problematic specialization
std::vector<char> vc{1, 0, 1}; // Better: standard container semantics
std::bitset<N> b; // C++11: best for fixed-size bit setsBounds checking: [] vs at()
operator[] performs no bounds check. Out-of-bounds access is undefined behavior and may silently corrupt memory:
std::vector<int> v{1, 2, 3};
v[10]; // UB — silent memory corruption in release builds
v.at(10); // Throws std::out_of_range — safe, but slowerExpensive moves and copies
Vector reallocations move or copy all elements. For large objects or tight loops, this is expensive. Pre-allocate with reserve():
struct Large { char data[1024]; };
std::vector<Large> v;
v.reserve(1000); // Single allocation; no reallocations during appendStale references after mutation
References obtained before any mutation become invalid if the vector reallocates:
std::vector<int> v{1, 2, 3};
int& ref = v[0];
v.push_back(4); // May reallocate
std::cout << ref; // Dangling reference — UBSee Also
std::array— Fixed-size array wrapper; no heap allocation; C++11std::deque— Double-ended queue; O(1) front and back insertion; worse cache localitystd::list— Doubly-linked list; O(n) random access; stable pointers/iteratorsstd::string— Specialization forchar; adds string-specific operations- Algorithms (
<algorithm>) —std::find,std::sort,std::erase,std::erase_if(C++20) - Memory (
<memory>) —std::allocator, custom allocation strategies - Iterators — Understanding invalidation rules is critical for correct vector usage