Intermediate 11 min

Vector - Dynamic Arrays

Master std::vector, the most versatile and commonly used STL container for dynamic array management

Learn how to use std::vector, the most versatile and commonly used container in C++, for dynamic array management with automatic memory handling.

A Simple Example

#include <iostream>
#include <vector>

int main() {
    // Creating vectors
    std::vector<int> numbers;              // Empty vector
    std::vector<int> primes{2, 3, 5, 7};  // Initialize with values
    std::vector<int> zeros(5);             // 5 elements, all zero
    std::vector<int> fives(5, 100);        // 5 elements, all 100

    // Adding elements
    numbers.push_back(10);
    numbers.push_back(20);
    numbers.push_back(30);

    // Accessing elements
    std::cout << "First: " << numbers[0] << "\n";        // No bounds check
    std::cout << "Second: " << numbers.at(1) << "\n";    // With bounds check

    // Size and capacity
    std::cout << "Size: " << numbers.size() << "\n";         // 3
    std::cout << "Capacity: " << numbers.capacity() << "\n";  // >= 3

    // Iterating
    for (const auto& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << "\n";

    return 0;
}

Breaking It Down

Creating and Initializing Vectors

  • Empty vector: std::vector<int> numbers; creates a vector with no elements
  • With values: std::vector<int> primes{2, 3, 5, 7}; initializes with specific values
  • Fixed size: std::vector<int> zeros(5); creates 5 zero-initialized elements
  • With value: std::vector<int> fives(5, 100); creates 5 elements, all set to 100

Adding and Accessing Elements

  • push_back(): Adds elements to the end of the vector
  • Operator []: Fast access without bounds checking (unsafe)
  • Method at(): Access with bounds checking, throws exception if out of range
  • Remember: Use at() when safety is critical, [] when performance matters

Size vs Capacity

  • size(): Returns the number of elements currently stored
  • capacity(): Returns the total allocated space before reallocation needed
  • Capacity is always >= size to minimize reallocations
  • Use reserve(n) to pre-allocate capacity when you know the final size

Iterating Through Vectors

  • Range-based for loop: for (const auto& item : vec) is clean and safe
  • Index-based: for (size_t i{0}; i < vec.size(); ++i) when you need indices
  • Iterators: for (auto it = vec.begin(); it != vec.end(); ++it) for advanced operations
  • Remember: Use const auto& to avoid unnecessary copies

Why This Matters

  • std::vector is the most important container in C++. It combines the performance of arrays with automatic memory management, handles resizing automatically, and provides bounds checking in debug mode.
  • It's the recommended default container choice unless you have a specific reason to use something else. Understanding vector is essential for writing modern C++ code.

Critical Insight

Vector doesn't reallocate every time you add an element. It grows by a factor (usually 1.5x or 2x) when it runs out of capacity. This means push_back() is O(1) amortized - most insertions are fast, and occasional reallocations are offset by the growth strategy.

If you know the final size, use reserve() to pre-allocate and avoid all reallocations. This is the secret to high-performance vector usage.

Best Practices

Use reserve() when you know the size: Call vec.reserve(1000) before adding elements to avoid multiple reallocations. This significantly improves performance for large vectors.

Prefer at() during development: Use at() for bounds checking while debugging. Switch to [] for performance-critical production code after verification.

Use emplace_back() for complex objects: Instead of push_back(), use emplace_back() to construct objects in-place, avoiding unnecessary copies.

Pass vectors by const reference: Use void func(const std::vector<int>& vec) to avoid expensive copies when passing vectors to functions.

Common Mistakes

Using [] Without Checking Bounds: vector[100] on a 10-element vector is undefined behavior. Use at() for safety or check size() first.

Iterator Invalidation: After push_back(), insert(), or erase(), iterators may be invalidated. Always refresh iterators after modifying the vector.

Comparing size() to signed int: Use vec.size() returns size_t (unsigned). Comparing with negative numbers or signed int can cause unexpected results.

Shrinking to fit: After removing elements, capacity doesn't decrease automatically. Use vec.shrink_to_fit() to release unused memory if needed.

Debug Challenge

This code tries to access an element that doesn't exist. Click the highlighted line to fix it:

1 #include <iostream>
2 #include <vector>
3
4 int main() {
5 std::vector<int> numbers{10, 20, 30};
6 std::cout << numbers[5] << "\n";
7 return 0;
8 }

Quick Quiz

  1. What's the difference between size() and capacity()?
`size()` is current elements, `capacity()` is allocated space
They're the same thing
`size()` is in bytes, `capacity()` is in elements
  1. What's the time complexity of push_back()?
O(1) amortized
O(1) always
O(n) always
  1. Which method should you use for safe element access with bounds checking?
at()
[]
front()
back()

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once