Core Mechanics and API Usage of C++ std::vector
std::vector implements a dynamically resizable sequence container backed by contiguous memory. This layout enables O(1) random access via indexing, matching raw array performance. Unlike static arrays, vectors manage their own memory footprint, automatically expanding when new elements exceed the current capacity.
Memory expansion follows a geometric growth strategy. When push_back or similar operations trigger a capacity breach, the container allocates a larger block, migrates existing elements, and deallocates the old buffer. This reallocation is computationally expensive, which is why vectors pre-allocate extra capacity. The growth factor varies by standard library implementation (commonly 1.5x or 2x), balancing memory overhead against allocation frequency. Consequently, vectors excel at random access and tail insertions/deletions, but mid-sequence modifications require element shifting and perform poorly compared to node-based containers like std::list.
As a class template, std::vector requires explicit type specification during instantiation:
std::vector<int> numbers;
std::vector<double> measurements;
std::vector<std::string> labels;
std::vector<std::vector<int>> matrix; // Jagged array structure
Note that std::vector<char> is not a substitute for std::string. The string class guarantees null-termination for C-API compatibility and provides specialized text manipulation interfaces, whereas a character vector treats data as raw bytes without implicit terminators.
Construction Patterns
Vectors support multiple initialization strategies tailored to different use cases.
Default and Fill Construction
#include <iostream>
#include <vector>
#include <iomanip>
int main() {
std::vector<int> counts(5, 42); // Five elements, each initialized to 42
std::vector<double> ratios(3); // Three elements, value-initialized to 0.0
std::vector<char> symbols(4, '#'); // Four elements, each '#'
for (int val : counts) std::cout << val << ' ';
std::cout << '\n';
std::cout << std::setprecision(4);
for (double val : ratios) std::cout << val << ' ';
std::cout << '\n';
for (char val : symbols) std::cout << val << ' ';
std::cout << '\n';
return 0;
}
Omitting the fill value triggers value-initialization (zero for arithmetic types, null for pointers, default constructor for class types).
Copy and Range Construction
#include <iostream>
#include <vector>
int main() {
std::vector<int> source{10, 20, 30, 40, 50};
std::vector<int> duplicate(source); // Copy constructor
std::vector<int> subset(source.begin() + 1, source.begin() + 4); // Range [20, 30, 40]
int raw_data[] = {7, 8, 9, 10};
std::vector<int> from_array(raw_data, raw_data + 4);
for (int v : subset) std::cout << v << ' ';
std::cout << '\n';
return 0;
}
Assignment (operator=) performs a deep copy identical to the copy constructor. Range constructors accept any valid iterator pair or pointer boundaries, enabling seamless conversion from C-style arrays.
Initializer List Construction
std::vector<int> primes{2, 3, 5, 7, 11};
std::vector<std::string> words{"alpha", "beta", "gamma"};
Iterator Mechanics
Vector iterators behave like raw pointers but provide a standardized abstraction. For std::vector, they are typically implemented as contiguous memory pointers.
#include <iostream>
#include <vector>
int main() {
std::vector<int> sequence{1, 2, 3, 4, 5};
auto fwd_begin = sequence.begin();
auto fwd_end = sequence.end();
std::cout << "First: " << *fwd_begin << '\n';
std::cout << "Last: " << *(fwd_end - 1) << '\n';
auto rev_begin = sequence.rbegin();
auto rev_end = sequence.rend();
while (rev_begin != rev_end) {
std::cout << *rev_begin << ' ';
++rev_begin; // Advances backwards logically
}
std::cout << '\n';
return 0;
}
Reverse iterators traverse the container backwards while still using the ++ operator for logical progression.
Capacity and Size Management
Distinguishing between logical size and allocated capacity is critical for performance tuning.
size(): Returns the number of actively stored elements.capacity(): Returns the total number of elements the current allocation can hold without reallocation.empty(): Returnstrueifsize() == 0.resize(n, val): Adjusts logical size. Shrinking discards trailing elements. Expanding appendsval(or value-initialized objects if omitted). Capacity remains unchanged unless expansion exceeds it.reserve(n): Requests minimum capacity. Only increases allocation; never shrinks. Prevents repeated reallocations during bulk insertions.
#include <iostream>
#include <vector>
int main() {
std::vector<int> buffer;
buffer.reserve(50); // Pre-allocate to avoid reallocation overhead
std::cout << "Size: " << buffer.size() << ", Capacity: " << buffer.capacity() << '\n';
for (int i = 0; i < 30; ++i) buffer.push_back(i);
std::cout << "After insertion -> Size: " << buffer.size() << ", Capacity: " << buffer.capacity() << '\n';
buffer.resize(10); // Truncates to first 10 elements
std::cout << "After resize -> Size: " << buffer.size() << ", Capacity: " << buffer.capacity() << '\n';
buffer.clear(); // Sets size to 0, capacity remains intact
std::cout << "After clear -> Size: " << buffer.size() << ", Capacity: " << buffer.capacity() << '\n';
return 0;
}
Element Access and Modification
Direct indexing via operator[] provides unchecked access. Bounds checking requires at(), which throws std::out_of_range on violation.
Insertion and Removal
#include <iostream>
#include <vector>
int main() {
std::vector<int> dataset{10, 20, 30};
dataset.push_back(40);
dataset.push_back(50);
// Insert single element at index 1
dataset.insert(dataset.begin() + 1, 15);
// Insert three copies of 99 at the end
dataset.insert(dataset.end(), 3, 99);
// Remove last element
dataset.pop_back();
// Remove element at index 2
dataset.erase(dataset.begin() + 2);
// Remove range [begin+1, begin+3)
dataset.erase(dataset.begin() + 1, dataset.begin() + 3);
for (int val : dataset) std::cout << val << ' ';
std::cout << '\n';
return 0;
}
insert and erase operate on iterator positions. Mid-container modifications shift subsequent elements, resulting in O(N) complexity. swap exchanges contants and capacity between two vectors in O(1) time.
Algorithm Integration
The <algorithm> header provides generic routines that operate on vector iterators.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> values{4, 1, 8, 3, 9, 2};
// Linear search
auto found = std::find(values.begin(), values.end(), 8);
if (found != values.end()) {
std::cout << "Located: " << *found << '\n';
}
// Ascending sort
std::sort(values.begin(), values.end());
for (int v : values) std::cout << v << ' ';
std::cout << '\n';
// Descending sort via reverse iterators
std::sort(values.rbegin(), values.rend());
for (int v : values) std::cout << v << ' ';
std::cout << '\n';
// In-place reversal
std::reverse(values.begin(), values.end());
for (int v : values) std::cout << v << ' ';
std::cout << '\n';
return 0;
}
Iterator Invalidation Rules
Iterators abstract underlying memory layouts, but operations that modify the container's structure can invalidate them. Using an invalidated iterator results in undefined behavior, typically manifesting as segmentation faults or data corruption.
Reallocation-Induced Invalidation
Any operation that increases capacity (e.g., push_back, insert, reserve, resize beyond current capacity) reallocates the internal buffer. All existing iterators, pointers, and references to elements become invalid.
std::vector<int> seq{1, 2, 3};
auto it = seq.begin();
seq.push_back(4); // May trigger reallocation
// *it is now undefined behavior. Reassign before use:
it = seq.begin();
Erasure-Induced Invalidation
erase removes elements and shifts trailing data forward. Iterators pointing to the removed element and all subsequent elements are invalidated. Iterators preceding the removal point remain valid.
std::vector<int> items{10, 20, 30, 40};
auto pos = std::find(items.begin(), items.end(), 30);
if (pos != items.end()) {
// erase returns a valid iterator to the element following the removed one
pos = items.erase(pos);
}
When erasing during iteration, always capture the return value of erase to maintain a valid traversal state. Similarly, std::string follows identical invalidation semantics due to its contiguous storage model. The universal mitigation strategy is to refresh iterators immediately after any mutating operation that affects capacity or element positions.