Vectors in C++: Dynamic Arrays Made Easy

Master C++ vectors—the most-used STL container. Learn initialization, insertion, deletion, iteration, resizing, and performance with practical step-by-step examples.

Vectors in C++: Dynamic Arrays Made Easy

A C++ vector (std::vector) is a dynamic array that automatically resizes itself as elements are added or removed. Unlike plain C-style arrays with fixed sizes, vectors manage their own memory, provide bounds-safe access, support O(1) random access by index, and integrate seamlessly with STL algorithms and iterators—making them the most commonly used container in modern C++ programming.

Introduction: The Array That Grows With You

Every programmer quickly encounters the fundamental limitation of C-style arrays: their size is fixed at compile time. Need to add a 101st element to an array of 100? You’re stuck. You’d have to allocate a bigger array, copy everything over, and manage the old memory yourself—tedious, error-prone work that distracts from solving the actual problem.

std::vector solves this completely. A vector is a sequence container that manages a dynamic array internally, automatically handling memory allocation, resizing, and deallocation. You can push elements onto it indefinitely; the vector grows as needed. When it goes out of scope, it cleans up after itself. This automatic resource management, combined with contiguous memory storage and O(1) random access, makes vector the go-to container for most C++ programming tasks.

In fact, vectors are so fundamental that a common piece of advice in the C++ community is: “When in doubt, use vector.” It offers the best combination of performance (cache-friendly contiguous memory), convenience (auto-resizing, STL integration), and safety (no raw pointer arithmetic) for the vast majority of use cases.

This comprehensive guide will teach you everything about vectors: how to create and initialize them, how to add and remove elements, how to navigate them with iterators, how capacity and size interact, how to avoid common pitfalls, and how to squeeze maximum performance out of them. Every concept is illustrated with practical, step-by-step examples so you understand not just the “what” but the “why.”

Creating and Initializing Vectors

Vectors offer many ways to be constructed and initialized. Choosing the right one makes your code more expressive and efficient.

C++
#include <iostream>
#include <vector>
#include <string>
using namespace std;

int main() {
    // 1. Default constructor — empty vector
    vector<int> v1;
    cout << "v1 (empty): size=" << v1.size() << endl;

    // 2. Constructor with initial size
    vector<int> v2(5);
    cout << "v2 (5 zeros): ";
    for (int n : v2) cout << n << " ";
    cout << endl;

    // 3. Constructor with size and fill value
    vector<int> v3(5, 42);
    cout << "v3 (5 forties-twos): ";
    for (int n : v3) cout << n << " ";
    cout << endl;

    // 4. Initializer list
    vector<int> v4 = {10, 20, 30, 40, 50};
    cout << "v4 (initializer list): ";
    for (int n : v4) cout << n << " ";
    cout << endl;

    // 5. Copy constructor
    vector<int> v5 = v4;
    cout << "v5 (copy of v4): ";
    for (int n : v5) cout << n << " ";
    cout << endl;

    // 6. From iterator range
    vector<int> v6(v4.begin() + 1, v4.end() - 1);
    cout << "v6 (middle slice of v4): ";
    for (int n : v6) cout << n << " ";
    cout << endl;

    // 7. Strings vector
    vector<string> words = {"hello", "world", "C++"};
    cout << "words: ";
    for (const string& w : words) cout << w << " ";
    cout << endl;

    // 8. Vector of vectors (2D)
    vector<vector<int>> grid(3, vector<int>(3, 0));
    grid[1][1] = 9;
    cout << "2D grid [1][1] = " << grid[1][1] << endl;

    return 0;
}

Step-by-step explanation:

  1. vector<int> v1: Default constructor creates an empty vector — size 0, capacity 0
  2. v1.size(): Returns number of elements currently stored — 0 for empty vector
  3. vector<int> v2(5): Size constructor — creates 5 elements, each value-initialized to 0
  4. Value initialization: Integers default to 0, strings to “”, pointers to nullptr
  5. vector<int> v3(5, 42): Fill constructor — creates 5 elements all set to the value 42
  6. Initializer list {}: Brace-initialization — most readable for known values at compile time
  7. vector<int> v5 = v4: Copy constructor — deep copy, v5 is independent of v4
  8. Range constructor: vector(begin, end) copies elements from any iterator range
  9. v4.begin() + 1: Points to second element (skips first)
  10. v4.end() – 1: Points to last element (stops before it)
  11. Slice result: v6 contains v4’s middle elements {20, 30, 40}
  12. vector<string>: Works with any type — vector is a template
  13. vector<vector<int>>: Nested vector creates a 2D structure (grid)
  14. grid[row][col]: Access 2D grid with double subscript operator
  15. Inner vector initialization: vector<int>(3, 0) creates a row of 3 zeros

Output:

Plaintext
v1 (empty): size=0
v2 (5 zeros): 0 0 0 0 0
v3 (5 forties-twos): 42 42 42 42 42
v4 (initializer list): 10 20 30 40 50
v5 (copy of v4): 10 20 30 40 50
v6 (middle slice of v4): 20 30 40
words: hello world C++
2D grid [1][1] = 9

Adding and Inserting Elements

Vectors provide multiple ways to add elements, each with different performance characteristics.

C++
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;

    // --- push_back: add to end ---
    cout << "=== push_back ===" << endl;
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);
    cout << "After push_back 10, 20, 30: ";
    for (int n : v) cout << n << " ";
    cout << endl;

    // --- emplace_back: construct in-place at end (more efficient) ---
    cout << "\n=== emplace_back ===" << endl;
    v.emplace_back(40);   // Constructs 40 directly — no temporary copy
    v.emplace_back(50);
    cout << "After emplace_back 40, 50: ";
    for (int n : v) cout << n << " ";
    cout << endl;

    // --- insert at specific position ---
    cout << "\n=== insert ===" << endl;
    auto it = v.begin() + 2;          // Iterator to index 2
    v.insert(it, 99);                  // Insert 99 before index 2
    cout << "After inserting 99 at index 2: ";
    for (int n : v) cout << n << " ";
    cout << endl;

    // --- insert multiple copies ---
    v.insert(v.begin(), 3, 0);         // Insert 3 zeros at beginning
    cout << "After inserting 3 zeros at start: ";
    for (int n : v) cout << n << " ";
    cout << endl;

    // --- insert a range ---
    vector<int> extra = {100, 200, 300};
    v.insert(v.end(), extra.begin(), extra.end());
    cout << "After inserting range {100,200,300} at end: ";
    for (int n : v) cout << n << " ";
    cout << endl;

    // --- assign: replace entire contents ---
    cout << "\n=== assign ===" << endl;
    v.assign(4, 7);                    // Replace with 4 sevens
    cout << "After assign(4, 7): ";
    for (int n : v) cout << n << " ";
    cout << endl;

    v.assign({1, 2, 3, 4, 5});         // Assign from initializer list
    cout << "After assign({1,2,3,4,5}): ";
    for (int n : v) cout << n << " ";
    cout << endl;

    return 0;
}

Step-by-step explanation:

  1. push_back(10): Appends 10 to the end of the vector — O(1) amortized
  2. Amortized O(1): Occasionally triggers a resize (O(n)) but on average each push_back is O(1)
  3. Three push_backs: Vector now holds {10, 20, 30}
  4. emplace_back(40): Constructs element directly in the vector’s memory — avoids temporary copy
  5. When emplace_back wins: For complex objects, emplace_back passes constructor args directly
  6. For ints: push_back and emplace_back are equivalent — difference matters for objects
  7. v.begin() + 2: Random-access iterator arithmetic, points to index 2 (value 30)
  8. insert(it, 99): Inserts 99 before the element at iterator position — O(n) due to shifting
  9. Elements shift right: All elements from index 2 onward move one position right
  10. insert(v.begin(), 3, 0): Inserts 3 copies of value 0 at the beginning
  11. Range insert: insert(pos, first, last) copies all elements from [first, last) at pos
  12. assign(4, 7): Destroys all existing elements and fills with 4 sevens
  13. assign from initializer list: Replaces contents with the listed values
  14. Iterator invalidation warning: After insert or assign, all previously held iterators become invalid

Output:

Plaintext
=== push_back ===
After push_back 10, 20, 30: 10 20 30

=== emplace_back ===
After emplace_back 40, 50: 10 20 30 40 50

=== insert ===
After inserting 99 at index 2: 10 20 99 30 40 50
After inserting 3 zeros at start: 0 0 0 10 20 99 30 40 50
After inserting range {100,200,300} at end: 0 0 0 10 20 99 30 40 50 100 200 300

=== assign ===
After assign(4, 7): 7 7 7 7
After assign({1,2,3,4,5}): 1 2 3 4 5

Accessing Elements

Vectors provide multiple safe and unsafe access methods. Knowing which to use is essential.

C++
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;

int main() {
    vector<int> v = {10, 20, 30, 40, 50};

    // --- Subscript operator [] (no bounds checking) ---
    cout << "=== Subscript operator [] ===" << endl;
    cout << "v[0] = " << v[0] << endl;
    cout << "v[2] = " << v[2] << endl;
    cout << "v[4] = " << v[4] << endl;
    // v[10] = 99;  // UNDEFINED BEHAVIOR — no bounds check, no exception

    // --- at() method (with bounds checking) ---
    cout << "\n=== at() method (safe) ===" << endl;
    cout << "v.at(0) = " << v.at(0) << endl;
    cout << "v.at(3) = " << v.at(3) << endl;

    try {
        cout << "v.at(10) = " << v.at(10) << endl;  // Out of bounds
    } catch (const out_of_range& e) {
        cout << "Exception caught: " << e.what() << endl;
    }

    // --- front() and back() ---
    cout << "\n=== front() and back() ===" << endl;
    cout << "Front: " << v.front() << endl;
    cout << "Back:  " << v.back() << endl;

    // --- data() — raw pointer to underlying array ---
    cout << "\n=== data() — raw pointer ===" << endl;
    int* raw = v.data();
    cout << "First via raw pointer: " << raw[0] << endl;
    cout << "Third via raw pointer: " << raw[2] << endl;
    // Useful when interfacing with C APIs expecting int*

    // --- Modifying via access ---
    cout << "\n=== Modifying elements ===" << endl;
    v[0] = 100;
    v.at(4) = 500;
    v.front() = 999;
    cout << "After modifications: ";
    for (int n : v) cout << n << " ";
    cout << endl;

    // --- Access on const vector ---
    cout << "\n=== Const vector access ===" << endl;
    const vector<int> cv = {1, 2, 3};
    cout << "cv[1] = " << cv[1] << endl;
    cout << "cv.at(2) = " << cv.at(2) << endl;
    // cv[0] = 99;  // ERROR: cannot assign to const

    return 0;
}

Step-by-step explanation:

  1. v[0]: Subscript operator — O(1) access, no bounds checking
  2. Fast but unsafe: If index >= size, behavior is undefined (crash or garbage)
  3. v[2]: Returns reference to element at index 2 — can be used as lvalue
  4. v.at(0): Bounds-checked access — throws std::out_of_range if index invalid
  5. at() is safer: Prefer at() during development/debugging for safety
  6. try-catch block: Catches the out_of_range exception thrown by v.at(10)
  7. e.what(): Returns a descriptive error message string
  8. v.front(): Returns reference to the first element — equivalent to v[0]
  9. v.back(): Returns reference to the last element — equivalent to v[v.size()-1]
  10. v.data(): Returns raw pointer to the underlying array — enables C interoperability
  11. raw[0]: Pointer arithmetic access, identical to v[0]
  12. C API use case: Many C functions require int* — data() provides it
  13. v[0] = 100: Subscript operator returns lvalue reference — supports assignment
  14. v.front() = 999: front() also returns lvalue reference — modifiable
  15. const vector<int> cv: Read-only vector — subscript and at() work, but no modification

Output:

Plaintext
=== Subscript operator [] ===
v[0] = 10
v[2] = 30
v[4] = 50

=== at() method (safe) ===
v.at(0) = 10
v.at(3) = 40
Exception caught: vector::_M_range_check: __n (which is 10) >= this->size() (which is 5)

=== front() and back() ===
Front: 10
Back:  50

=== data() — raw pointer ===
First via raw pointer: 10
Third via raw pointer: 30

=== Modifying elements ===
After modifications: 999 20 30 40 500

=== Const vector access ===
cv[1] = 2
cv.at(2) = 3

Removing Elements

Vectors support several removal strategies, each with different complexity.

C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(const vector<int>& v, const string& label) {
    cout << label << ": ";
    for (int n : v) cout << n << " ";
    cout << "(size=" << v.size() << ")" << endl;
}

int main() {
    // --- pop_back: remove last element ---
    cout << "=== pop_back ===" << endl;
    vector<int> v = {10, 20, 30, 40, 50};
    print(v, "Before");
    v.pop_back();
    print(v, "After pop_back");

    // --- erase single element by iterator ---
    cout << "\n=== erase (single element) ===" << endl;
    v = {10, 20, 30, 40, 50};
    print(v, "Before");
    v.erase(v.begin() + 2);    // Remove element at index 2 (value 30)
    print(v, "After erase index 2");

    // --- erase range ---
    cout << "\n=== erase (range) ===" << endl;
    v = {10, 20, 30, 40, 50};
    print(v, "Before");
    v.erase(v.begin() + 1, v.begin() + 4);   // Remove indices 1, 2, 3
    print(v, "After erase [1,4)");

    // --- clear: remove all elements ---
    cout << "\n=== clear ===" << endl;
    v = {10, 20, 30};
    print(v, "Before");
    v.clear();
    print(v, "After clear");
    cout << "Capacity after clear: " << v.capacity() << endl;  // Capacity unchanged!

    // --- Erase-remove idiom: remove by value ---
    cout << "\n=== Erase-Remove Idiom ===" << endl;
    v = {1, 2, 3, 2, 4, 2, 5};
    print(v, "Before");
    v.erase(remove(v.begin(), v.end(), 2), v.end());
    print(v, "After removing all 2s");

    // --- Erase-remove with predicate ---
    cout << "\n=== Erase-Remove with Predicate ===" << endl;
    v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    print(v, "Before");
    v.erase(
        remove_if(v.begin(), v.end(), [](int n){ return n % 2 == 0; }),
        v.end()
    );
    print(v, "After removing all even numbers");

    return 0;
}

Step-by-step explanation:

  1. pop_back(): Removes the last element — O(1), no shifting required
  2. pop_back does not return: Unlike stack’s pop, vector’s pop_back returns void — read back() first
  3. erase(iterator): Removes element at the iterator position — O(n) due to left-shift
  4. v.begin() + 2: Points to element at index 2 (value 30)
  5. Elements shift left: After erasing index 2, former index 3 becomes index 2
  6. erase(first, last): Removes range [first, last) — half-open interval
  7. Erasing [1,4): Removes elements at indices 1, 2, and 3 — three elements removed
  8. clear(): Sets size to 0, removes all elements — O(n) (destructors called)
  9. Capacity unchanged: clear() does NOT release memory — capacity stays allocated
  10. remove(begin, end, 2): Moves all non-2 elements to the front, returns new logical end
  11. remove doesn’t erase: It rearranges elements — actual erasure requires erase()
  12. Erase-remove idiom: The two-step remove() + erase() pattern is idiomatic C++
  13. remove_if(begin, end, pred): Moves elements not matching predicate to front
  14. Lambda predicate: [](int n){ return n % 2 == 0; } returns true for even numbers
  15. Combined operation: remove_if + erase efficiently removes all matching elements

Output:

Plaintext
=== pop_back ===
Before: 10 20 30 40 50 (size=5)
After pop_back: 10 20 30 40 (size=4)

=== erase (single element) ===
Before: 10 20 30 40 50 (size=5)
After erase index 2: 10 20 40 50 (size=4)

=== erase (range) ===
Before: 10 20 30 40 50 (size=5)
After erase [1,4): 10 50 (size=2)

=== clear ===
Before: 10 20 30 (size=3)
After clear:  (size=0)
Capacity after clear: 3

=== Erase-Remove Idiom ===
Before: 1 2 3 2 4 2 5 (size=7)
After removing all 2s: 1 3 4 5 (size=4)

=== Erase-Remove with Predicate ===
Before: 1 2 3 4 5 6 7 8 9 10 (size=10)
After removing all even numbers: 1 3 5 7 9 (size=5)

Size vs Capacity: Understanding Vector Growth

Understanding the relationship between size and capacity is key to writing efficient vector code.

C++
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;

    cout << "=== Watching size and capacity grow ===" << endl;
    cout << "Initial — size: " << v.size()
         << ", capacity: " << v.capacity() << endl;

    for (int i = 1; i <= 16; i++) {
        v.push_back(i);
        cout << "After push_back(" << i << ") — size: " << v.size()
             << ", capacity: " << v.capacity() << endl;
    }

    // --- reserve: pre-allocate capacity ---
    cout << "\n=== reserve: pre-allocate capacity ===" << endl;
    vector<int> v2;
    v2.reserve(100);                   // Allocate space for 100 elements
    cout << "After reserve(100) — size: " << v2.size()
         << ", capacity: " << v2.capacity() << endl;

    for (int i = 0; i < 10; i++) v2.push_back(i);
    cout << "After 10 push_backs — size: " << v2.size()
         << ", capacity: " << v2.capacity() << endl;  // Still 100

    // --- resize: change size ---
    cout << "\n=== resize ===" << endl;
    vector<int> v3 = {1, 2, 3};
    cout << "Before resize(6) — size: " << v3.size() << endl;
    v3.resize(6);                      // Grow — new elements value-initialized to 0
    cout << "After resize(6): ";
    for (int n : v3) cout << n << " ";
    cout << endl;

    v3.resize(2);                      // Shrink — removes elements beyond index 2
    cout << "After resize(2): ";
    for (int n : v3) cout << n << " ";
    cout << endl;

    // --- shrink_to_fit: release excess capacity ---
    cout << "\n=== shrink_to_fit ===" << endl;
    vector<int> v4;
    v4.reserve(1000);
    for (int i = 0; i < 5; i++) v4.push_back(i);
    cout << "Before shrink — size: " << v4.size()
         << ", capacity: " << v4.capacity() << endl;

    v4.shrink_to_fit();
    cout << "After shrink_to_fit — size: " << v4.size()
         << ", capacity: " << v4.capacity() << endl;

    return 0;
}

Step-by-step explanation:

  1. Initial state: Empty vector — size 0, capacity 0 (no allocation yet)
  2. First push_back: Triggers first allocation, typically capacity becomes 1
  3. Doubling strategy: Most implementations double capacity when full (1→2→4→8→16→…)
  4. Doubling amortizes cost: Reallocation happens infrequently — total cost is O(n)
  5. Size vs capacity: Size = actual elements; capacity = allocated memory (always >= size)
  6. Reallocation copies: When capacity exceeded, new larger array allocated, all elements copied
  7. reserve(100): Allocates memory for 100 elements — no elements added, size stays 0
  8. Benefit of reserve: Prevents reallocations when you know max size in advance
  9. Capacity stays 100: push_backs don’t trigger reallocation while size < capacity
  10. resize(6): Grows size to 6, new elements default-initialized (0 for int)
  11. resize(2): Shrinks size to 2 — elements beyond index 2 are destroyed
  12. Capacity may remain: resize() does not reduce capacity
  13. shrink_to_fit(): Requests capacity be reduced to match size (non-binding hint)
  14. Memory release: After shrink_to_fit(), capacity should equal size
  15. When to use reserve(): Whenever you know approximate final size — avoids O(n log n) reallocations

Output (capacity doubling pattern may vary by implementation):

Plaintext
=== Watching size and capacity grow ===
Initial — size: 0, capacity: 0
After push_back(1) — size: 1, capacity: 1
After push_back(2) — size: 2, capacity: 2
After push_back(3) — size: 3, capacity: 4
After push_back(4) — size: 4, capacity: 4
After push_back(5) — size: 5, capacity: 8
After push_back(6) — size: 6, capacity: 8
After push_back(7) — size: 7, capacity: 8
After push_back(8) — size: 8, capacity: 8
After push_back(9) — size: 9, capacity: 16
After push_back(10) — size: 10, capacity: 16
After push_back(11) — size: 11, capacity: 16
After push_back(12) — size: 12, capacity: 16
After push_back(13) — size: 13, capacity: 16
After push_back(14) — size: 14, capacity: 16
After push_back(15) — size: 15, capacity: 16
After push_back(16) — size: 16, capacity: 16

=== reserve: pre-allocate capacity ===
After reserve(100) — size: 0, capacity: 100
After 10 push_backs — size: 10, capacity: 100

=== resize ===
Before resize(6) — size: 3
After resize(6): 1 2 3 0 0 0
After resize(2): 1 2

=== shrink_to_fit ===
Before shrink — size: 5, capacity: 1000
After shrink_to_fit — size: 5, capacity: 5

Iterating Over Vectors

Vectors support several iteration styles—choosing the right one improves clarity and safety.

C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v = {10, 20, 30, 40, 50};

    // --- Style 1: Index-based (classic) ---
    cout << "=== Index-based loop ===" << endl;
    for (size_t i = 0; i < v.size(); i++) {
        cout << "v[" << i << "] = " << v[i] << endl;
    }

    // --- Style 2: Range-based for (modern, preferred) ---
    cout << "\n=== Range-based for loop ===" << endl;
    for (int n : v) {
        cout << n << " ";
    }
    cout << endl;

    // --- Style 3: Range-based with const reference (efficient for objects) ---
    cout << "\n=== Range-based with const reference ===" << endl;
    for (const int& n : v) {
        cout << n << " ";
    }
    cout << endl;

    // --- Style 4: Iterator-based ---
    cout << "\n=== Iterator-based loop ===" << endl;
    for (auto it = v.begin(); it != v.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // --- Style 5: Reverse iteration ---
    cout << "\n=== Reverse iteration ===" << endl;
    for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;

    // --- Style 6: for_each algorithm ---
    cout << "\n=== for_each with lambda ===" << endl;
    for_each(v.begin(), v.end(), [](int n) {
        cout << n * n << " ";   // Print square
    });
    cout << endl;

    // --- Modifying during iteration ---
    cout << "\n=== Modifying via range-based for ===" << endl;
    for (int& n : v) {     // Note: reference, not copy
        n += 5;
    }
    cout << "After adding 5 to each: ";
    for (int n : v) cout << n << " ";
    cout << endl;

    return 0;
}

Step-by-step explanation:

  1. Index-based loop: Classic C style — use when you need the index value
  2. size_t: Unsigned integer type returned by size() — avoids signed/unsigned comparison warning
  3. Range-based for: Cleanest syntax — recommended default for read-only iteration
  4. for (int n : v): Copies each element — fine for primitives, expensive for large objects
  5. for (const int& n : v): Reference to element — no copy, read-only (const)
  6. When to use const ref: For objects (strings, structs) to avoid unnecessary copies
  7. Iterator-based: Most verbose but most flexible — needed for insert/erase during loop
  8. auto it: Type deduction avoids spelling out vector<int>::iterator
  9. it != v.end(): Idiomatic iterator loop condition — not < comparison
  10. v.rbegin() / v.rend(): Reverse iterators — traverse from back to front
  11. for_each: Algorithm-based iteration — accepts any callable including lambdas
  12. Lambda [](int n){}: Inline function applied to each element
  13. for (int& n : v): Non-const reference — modifications propagate to the vector
  14. Avoid modifying while iterating with erase: Can invalidate iterators — use erase-remove idiom instead

Output:

Plaintext
=== Index-based loop ===
v[0] = 10
v[1] = 20
v[2] = 30
v[3] = 40
v[4] = 50

=== Range-based for loop ===
10 20 30 40 50

=== Range-based with const reference ===
10 20 30 40 50

=== Iterator-based loop ===
10 20 30 40 50

=== Reverse iteration ===
50 40 30 20 10

=== for_each with lambda ===
100 400 900 1600 2500

=== Modifying via range-based for ===
After adding 5 to each: 15 25 35 45 55

Vectors with Custom Objects

Vectors work with any type—including your own classes. Here’s how to use them with custom objects.

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

struct Student {
    string name;
    int grade;

    Student(string n, int g) : name(n), grade(g) {}

    void display() const {
        cout << name << " (grade: " << grade << ")";
    }
};

int main() {
    // --- Vector of objects ---
    cout << "=== Vector of Students ===" << endl;
    vector<Student> students;

    students.emplace_back("Alice", 92);     // Construct in-place
    students.emplace_back("Bob", 78);
    students.emplace_back("Charlie", 85);
    students.emplace_back("Diana", 95);
    students.emplace_back("Eve", 88);

    cout << "All students:" << endl;
    for (const Student& s : students) {
        cout << "  ";
        s.display();
        cout << endl;
    }

    // --- Sort by grade descending ---
    cout << "\n=== Sorted by grade (highest first) ===" << endl;
    sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
        return a.grade > b.grade;
    });
    for (const Student& s : students) {
        cout << "  ";
        s.display();
        cout << endl;
    }

    // --- Find specific student ---
    cout << "\n=== Finding Charlie ===" << endl;
    auto it = find_if(students.begin(), students.end(), [](const Student& s) {
        return s.name == "Charlie";
    });
    if (it != students.end()) {
        cout << "Found: ";
        it->display();
        cout << endl;
    }

    // --- Filter: students with grade >= 90 ---
    cout << "\n=== Top students (grade >= 90) ===" << endl;
    vector<Student> topStudents;
    copy_if(students.begin(), students.end(), back_inserter(topStudents),
            [](const Student& s) { return s.grade >= 90; });

    for (const Student& s : topStudents) {
        cout << "  ";
        s.display();
        cout << endl;
    }

    // --- Compute average grade ---
    cout << "\n=== Average grade ===" << endl;
    double total = 0;
    for (const Student& s : students) total += s.grade;
    double average = total / students.size();
    cout << "Average grade: " << average << endl;

    return 0;
}

Step-by-step explanation:

  1. struct Student: Simple aggregate type with name and grade
  2. Constructor: Initializes name and grade via member initializer list
  3. vector<Student>: Specializes vector template for Student type
  4. emplace_back(“Alice”, 92): Constructs Student directly in vector — passes args to constructor
  5. emplace_back vs push_back: emplace_back avoids creating a temporary Student object
  6. const Student& s: Read-only reference in range-for — efficient for objects
  7. s.display(): Calls member function through reference
  8. sort with lambda: Custom comparator sorts by grade descending
  9. a.grade > b.grade: Returns true when a should come before b — highest grade first
  10. find_if(): Searches for first element matching predicate
  11. Lambda predicate: s.name == "Charlie" — string comparison as search criterion
  12. it->display(): Arrow operator on iterator — equivalent to (*it).display()
  13. copy_if(): Copies elements matching predicate to output range
  14. back_inserter: Grows topStudents as each matching element is copied
  15. Total accumulation: Manual for-loop computes sum, then divides by size()

Output:

Plaintext
=== Vector of Students ===
All students:
  Alice (grade: 92)
  Bob (grade: 78)
  Charlie (grade: 85)
  Diana (grade: 95)
  Eve (grade: 88)

=== Sorted by grade (highest first) ===
  Diana (grade: 95)
  Alice (grade: 92)
  Eve (grade: 88)
  Charlie (grade: 85)
  Bob (grade: 78)

=== Finding Charlie ===
Found: Charlie (grade: 85)

=== Top students (grade >= 90) ===
  Diana (grade: 95)
  Alice (grade: 92)

=== Average grade ===-
Average grade: 87.6

Common Vector Pitfalls and How to Avoid Them

C++
#include <iostream>
#include <vector>
using namespace std;

int main() {
    // PITFALL 1: Iterator invalidation after push_back
    cout << "=== Pitfall 1: Iterator Invalidation ===" << endl;
    vector<int> v = {1, 2, 3};
    // auto it = v.begin();    // Saved iterator
    // v.push_back(4);         // May trigger reallocation!
    // cout << *it;            // UNDEFINED BEHAVIOR — it may be dangling
    cout << "Solution: Don't save iterators across push_back calls." << endl;
    cout << "Or call reserve() first to prevent reallocation." << endl;

    // PITFALL 2: Out-of-bounds with []
    cout << "\n=== Pitfall 2: Unchecked Access ===" << endl;
    vector<int> v2 = {10, 20, 30};
    // cout << v2[5];   // UNDEFINED BEHAVIOR — no exception, random data
    cout << "Solution: Use v.at(5) during development — throws out_of_range." << endl;

    // PITFALL 3: Inefficient push_back in loop (no reserve)
    cout << "\n=== Pitfall 3: No Reserve Before Loop ===" << endl;
    const int N = 1000000;
    vector<int> bad;
    // for (int i = 0; i < N; i++) bad.push_back(i);  // Multiple reallocations

    vector<int> good;
    good.reserve(N);     // One allocation
    for (int i = 0; i < N; i++) good.push_back(i);  // No reallocations
    cout << "Reserved vector — capacity: " << good.capacity() << endl;

    // PITFALL 4: Erasing during loop
    cout << "\n=== Pitfall 4: Erasing During Iteration ===" << endl;
    vector<int> v3 = {1, 2, 3, 2, 4, 2, 5};
    // WRONG — skips elements
    // for (auto it = v3.begin(); it != v3.end(); ++it) {
    //     if (*it == 2) v3.erase(it);   // it now invalid!
    // }
    
    // CORRECT — erase returns valid next iterator
    for (auto it = v3.begin(); it != v3.end(); ) {
        if (*it == 2) {
            it = v3.erase(it);   // erase returns iterator to next element
        } else {
            ++it;
        }
    }
    cout << "After safe erase of 2s: ";
    for (int n : v3) cout << n << " ";
    cout << endl;

    // PITFALL 5: Copying when move is intended
    cout << "\n=== Pitfall 5: Unnecessary Copies ===" << endl;
    vector<int> source = {1, 2, 3, 4, 5};
    vector<int> moved = move(source);        // Move — O(1), source is now empty
    cout << "Moved vector size: " << moved.size() << endl;
    cout << "Source after move: size=" << source.size() << endl;

    return 0;
}

Step-by-step explanation:

  1. Iterator invalidation: push_back may trigger reallocation, moving all data to new memory
  2. Dangling iterator: Old iterator points to freed memory after reallocation
  3. Solution: Use reserve() before push_back loop, or don’t save iterators across mutations
  4. Unchecked []: v[5] on a 3-element vector accesses arbitrary memory — no error thrown
  5. at() for safety: Bounds-checked access throws std::out_of_range — use during development
  6. No reserve anti-pattern: Each push_back may trigger O(n) reallocation — total O(n log n)
  7. With reserve(): One O(n) allocation upfront, then O(1) push_backs — total O(n)
  8. Erasing during loop: erase() invalidates the iterator, and ++it would skip or crash
  9. erase() return value: Returns iterator to the element that replaced the erased one
  10. Safe pattern: it = v.erase(it) reuses the returned valid iterator
  11. Else branch: Only advance iterator if no erasure happened — avoids skipping elements
  12. std::move(): Transfers ownership of internal buffer — O(1) operation
  13. Source emptied: After move, source vector is valid but empty (size=0)
  14. When to move: Moving instead of copying large vectors is a major performance win

Output:

Plaintext
=== Pitfall 1: Iterator Invalidation ===
Solution: Don't save iterators across push_back calls.
Or call reserve() first to prevent reallocation.

=== Pitfall 2: Unchecked Access ===
Solution: Use v.at(5) during development — throws out_of_range.

=== Pitfall 3: No Reserve Before Loop ===
Reserved vector — capacity: 1000000

=== Pitfall 4: Erasing During Iteration ===
After safe erase of 2s: 1 3 4 5

=== Pitfall 5: Unnecessary Copies ===
Moved vector size: 5
Source after move: size=0

Vector Quick Reference

OperationMethodComplexity
Access by indexv[i], v.at(i)O(1)
First / last elementv.front(), v.back()O(1)
Add to endpush_back(), emplace_back()O(1) amortized
Remove from endpop_back()O(1)
Insert at positioninsert(it, val)O(n)
Remove at positionerase(it)O(n)
Remove allclear()O(n)
Number of elementssize()O(1)
Allocated capacitycapacity()O(1)
Pre-allocatereserve(n)O(n)
Change sizeresize(n)O(n)
Release excessshrink_to_fit()O(n)
Empty checkempty()O(1)

Conclusion: Vectors as Your Default Container

The C++ vector is the workhorse of the STL—flexible, efficient, and safe. Its combination of O(1) random access, automatic resizing, and seamless integration with STL algorithms makes it the right choice for the majority of data storage needs in C++.

Key takeaways:

  • Prefer vector over raw arrays for almost all dynamic collections
  • Use emplace_back() over push_back() when constructing objects in-place
  • Use at() during development for bounds checking; switch to [] for performance-critical paths
  • Always call reserve() before a push_back loop when you know the final size
  • Use the erase-remove idiom (remove() + erase()) to delete elements by value or predicate
  • Understand size vs capacity — they serve different purposes
  • Iterator invalidation happens after any reallocation-triggering operation — don’t store iterators across mutations
  • Prefer range-based for loops with const T& for read-only iteration of objects
  • Use std::move() when transferring ownership to avoid expensive copies

Once you master vectors, you have a solid foundation for understanding the rest of the STL. The patterns you learn here—iterator-based algorithms, lambdas as predicates, erase-remove idiom, reserve for performance—apply throughout the entire library. Make vector your default container, and you’ll write cleaner, safer, and more efficient C++ from day one.

Share:
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments

Discover More

Top Data Science Bootcamps Compared: Which is Right for You?

Compare top data science bootcamps including curriculum, cost, outcomes, and learning formats. Discover which bootcamp…

Vectors and Matrices Explained for Robot Movement

Learn how vectors and matrices control robot movement. Understand position, velocity, rotation, and transformations with…

The Basics of Soldering: How to Create Permanent Connections

The Basics of Soldering: How to Create Permanent Connections

Learn soldering basics from equipment selection to technique, temperature, and finishing touches to create reliable…

Exploring Capacitors: Types and Capacitance Values

Discover the different types of capacitors, their capacitance values, and applications. Learn how capacitors function…

Kindred Raises $125M for Peer-to-Peer Home Exchange Platform

Travel platform Kindred raises $125 million across Series B and C rounds for peer-to-peer home…

Understanding Transistors: The Building Blocks of Modern Electronics

Understanding Transistors: The Building Blocks of Modern Electronics

Learn what transistors are, how BJTs and MOSFETs work, why they’re the foundation of all…

Click For More
0
Would love your thoughts, please comment.x
()
x