C++ std::vector: Complete Guide with Examples
Why std::vector?
C-style arrays have a fundamental limitation: their size is fixed at compile time. If you need a collection that grows or shrinks at runtime, you need std::vector. It is the most widely used container in C++ — the default choice for any sequence of elements.
std::vector manages its own memory automatically. It allocates space on the heap, grows when you add elements, and frees everything when it goes out of scope. You get the cache-friendly contiguous memory of arrays with the safety and flexibility of a dynamic container. The C++ Core Guidelines say: “Prefer vector by default.”
Creating Vectors
#include <iostream>
#include <vector>
using namespace std;
int main() {
// Empty vector
vector<int> v1;
// Vector with 5 elements, all zero
vector<int> v2(5); // {0, 0, 0, 0, 0}
// Vector with 5 elements, all set to 42
vector<int> v3(5, 42); // {42, 42, 42, 42, 42}
// Initializer list (C++11)
vector<int> v4 = {10, 20, 30, 40, 50};
vector<int> v5{1, 2, 3};
// Copy constructor
vector<int> v6(v4); // Copy of v4
// From another range
vector<int> v7(v4.begin(), v4.begin() + 3); // {10, 20, 30}
cout << "v3 size: " << v3.size() << endl;
cout << "v4 size: " << v4.size() << endl;
cout << "v7: ";
for (int x : v7) cout << x << " ";
cout << endl;
return 0;
}
Accessing Elements
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {10, 20, 30, 40, 50};
// operator[] — no bounds checking (fast but unsafe)
cout << nums[0] << endl; // 10
cout << nums[4] << endl; // 50
// .at() — throws std::out_of_range on invalid index
cout << nums.at(2) << endl; // 30
try {
cout << nums.at(10) << endl;
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
// front() and back()
cout << "First: " << nums.front() << endl; // 10
cout << "Last: " << nums.back() << endl; // 50
// data() — pointer to underlying array
int* raw = nums.data();
cout << "raw[1] = " << raw[1] << endl; // 20
return 0;
}
Adding Elements
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
// push_back: add to the end
v.push_back(10);
v.push_back(20);
v.push_back(30);
// v = {10, 20, 30}
// insert: add at a specific position
v.insert(v.begin() + 1, 15);
// v = {10, 15, 20, 30}
// insert multiple elements
v.insert(v.end(), {40, 50, 60});
// v = {10, 15, 20, 30, 40, 50, 60}
// assign: replace all contents
vector<int> v2;
v2.assign(5, 99); // {99, 99, 99, 99, 99}
v2.assign({1, 2, 3}); // {1, 2, 3}
for (int x : v) cout << x << " ";
cout << endl;
return 0;
}
Removing Elements
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {10, 20, 30, 40, 50, 60};
// pop_back: remove last element
v.pop_back(); // {10, 20, 30, 40, 50}
// erase: remove at position
v.erase(v.begin() + 1); // Remove 20 -> {10, 30, 40, 50}
// erase range
v.erase(v.begin(), v.begin() + 2); // Remove first 2 -> {40, 50}
// clear: remove all
// v.clear();
// Erase-remove idiom: remove all occurrences of a value
vector<int> nums = {1, 3, 2, 3, 4, 3, 5};
nums.erase(remove(nums.begin(), nums.end(), 3), nums.end());
// nums = {1, 2, 4, 5}
for (int x : nums) cout << x << " ";
cout << endl;
// C++20: std::erase(nums, 4); // Even simpler
return 0;
}
Size vs Capacity
Size is the number of elements currently in the vector. Capacity is the total space allocated. When size exceeds capacity, the vector reallocates (typically doubling) and copies everything to a new block.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
cout << "Size: " << v.size() << " Capacity: " << v.capacity() << endl;
for (int i = 0; i < 20; i++) {
v.push_back(i);
cout << "After push " << i << ": size=" << v.size()
<< " capacity=" << v.capacity() << endl;
}
// reserve: pre-allocate capacity (avoids reallocations)
vector<int> big;
big.reserve(10000); // Allocates space for 10000 elements
cout << "Reserved: size=" << big.size()
<< " capacity=" << big.capacity() << endl;
// shrink_to_fit: release unused capacity
big.push_back(1);
big.push_back(2);
big.shrink_to_fit();
cout << "Shrunk: size=" << big.size()
<< " capacity=" << big.capacity() << endl;
return 0;
}
If you know the final size in advance, call reserve() upfront. This avoids repeated reallocations and copies, which can be a huge performance win for large vectors.
Iterating Over Vectors
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<string> names = {"Alice", "Bob", "Charlie", "Diana"};
// Range-based for (preferred)
for (const auto& name : names) {
cout << name << " ";
}
cout << endl;
// Index-based
for (size_t i = 0; i < names.size(); i++) {
cout << i << ": " << names[i] << endl;
}
// Iterator-based
for (auto it = names.begin(); it != names.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// Reverse iteration
for (auto it = names.rbegin(); it != names.rend(); ++it) {
cout << *it << " ";
}
cout << endl;
return 0;
}
Using STL Algorithms
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
vector<int> v = {30, 10, 50, 20, 40};
// Sort
sort(v.begin(), v.end()); // {10, 20, 30, 40, 50}
// Reverse
reverse(v.begin(), v.end()); // {50, 40, 30, 20, 10}
// Find
auto it = find(v.begin(), v.end(), 30);
if (it != v.end()) {
cout << "Found 30 at index " << distance(v.begin(), it) << endl;
}
// Min/Max
cout << "Min: " << *min_element(v.begin(), v.end()) << endl;
cout << "Max: " << *max_element(v.begin(), v.end()) << endl;
// Sum
int total = accumulate(v.begin(), v.end(), 0);
cout << "Sum: " << total << endl;
// Count
vector<int> nums = {1, 2, 3, 2, 4, 2, 5};
cout << "Count of 2: " << count(nums.begin(), nums.end(), 2) << endl;
// Remove duplicates (must be sorted first)
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
for (int x : nums) cout << x << " ";
cout << endl;
return 0;
}
2D Vectors
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 3x4 matrix initialized to 0
vector<vector<int>> matrix(3, vector<int>(4, 0));
// Set values
matrix[0][0] = 1;
matrix[1][2] = 5;
matrix[2][3] = 9;
// Print
for (const auto& row : matrix) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
// Jagged array (rows of different sizes)
vector<vector<int>> jagged;
jagged.push_back({1, 2, 3});
jagged.push_back({4, 5});
jagged.push_back({6, 7, 8, 9});
for (const auto& row : jagged) {
for (int v : row) cout << v << " ";
cout << endl;
}
return 0;
}
Passing Vectors to Functions
#include <iostream>
#include <vector>
using namespace std;
// Read-only: const reference (no copy, no modification)
double average(const vector<int>& nums) {
double sum = 0;
for (int n : nums) sum += n;
return sum / nums.size();
}
// Modify in place: reference
void double_all(vector<int>& nums) {
for (int& n : nums) n *= 2;
}
// Return a new vector (move semantics make this efficient)
vector<int> filter_even(const vector<int>& nums) {
vector<int> result;
for (int n : nums) {
if (n % 2 == 0) result.push_back(n);
}
return result; // Moved, not copied (RVO/NRVO)
}
int main() {
vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8};
cout << "Average: " << average(data) << endl;
vector<int> evens = filter_even(data);
for (int x : evens) cout << x << " ";
cout << endl;
double_all(data);
for (int x : data) cout << x << " ";
cout << endl;
return 0;
}
Move Semantics with Vectors
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> original = {1, 2, 3, 4, 5};
// Copy: duplicates all data
vector<int> copied = original;
cout << "Original size after copy: " << original.size() << endl; // 5
// Move: transfers ownership, original becomes empty
vector<int> moved = std::move(original);
cout << "Original size after move: " << original.size() << endl; // 0
cout << "Moved size: " << moved.size() << endl; // 5
// Move is O(1) — just pointer swap
// Copy is O(n) — duplicates every element
return 0;
}
emplace_back vs push_back
#include <iostream>
#include <vector>
#include <string>
using namespace std;
struct Point {
double x, y;
Point(double x, double y) : x(x), y(y) {
cout << "Constructed (" << x << ", " << y << ")" << endl;
}
};
int main() {
vector<Point> points;
// push_back: constructs Point, then moves/copies into vector
points.push_back(Point(1.0, 2.0));
// emplace_back: constructs directly in the vector's memory
points.emplace_back(3.0, 4.0); // Forwards args to constructor
// For simple types, no difference
vector<int> nums;
nums.push_back(42); // Fine
nums.emplace_back(42); // Also fine — same performance
// emplace_back shines with complex objects that are expensive to move
vector<string> names;
names.emplace_back("Alice"); // Constructs string in-place
return 0;
}
Common Mistakes
1. Iterator invalidation:
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 3) v.erase(it); // BUG: iterator invalidated after erase!
}
// Fix: it = v.erase(it); and don't increment in the for header
2. Using [] on an empty vector:
vector<int> v;
// v[0] = 10; // UB — vector is empty, no element at index 0
v.push_back(10); // Correct way to add elements
3. Confusing resize and reserve:
vector<int> v;
v.reserve(100); // Capacity=100, size=0 — no elements exist yet
v.resize(100); // Capacity>=100, size=100 — 100 zero-initialized elements
Practice Exercises
Exercise 1: Write a function that takes a vector<int> and returns a new vector with duplicates removed (preserving order).
Exercise 2: Implement a simple stack using vector (push, pop, top, isEmpty).
Exercise 3: Write a function that rotates a vector left by k positions.
Exercise 4: Create a function that merges two sorted vectors into one sorted vector.
Summary
std::vector is the workhorse container of C++. It provides dynamic sizing, automatic memory management, bounds-checked access with .at(), seamless integration with STL algorithms, and efficient move semantics. Use reserve() when you know the size upfront, prefer emplace_back for complex objects, and watch out for iterator invalidation. In the next lesson, you will master C++ strings — from std::string basics to advanced manipulation.