C++ vector std::vector dynamic array tutorial

C++ std::vector: Complete Guide with Examples

Back to C++ RoadmapC++ Programming Course • 65 Lessons

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *