Skip to content
C++ Better Explained
Go back

C++ Vector Tutorial: The Complete Guide to std::vector for Beginners

Edit page

C++ Vector Tutorial: The Complete Guide to std::vector for Beginners

If you’ve been learning C++, you’ve probably heard that you should use std::vector instead of raw arrays. But what exactly is a vector, and how does it work?

By the end of this guide, you’ll understand std::vector deeply — what it does, how to use it, when to use it, and the common mistakes to avoid. You’ll also see plenty of real, runnable code examples at every step.

What Is std::vector?

A std::vector is a dynamic array — an array that can grow and shrink at runtime.

With a regular C++ array, you must decide the size upfront:

int scores[10]; // Fixed: can only ever hold 10 scores

That’s a problem. What if you don’t know how many scores you’ll have? What if the user can add or remove items? A fixed array can’t handle that.

A vector solves this:

#include <vector>

std::vector<int> scores; // Can hold any number of scores
scores.push_back(95);    // Add one
scores.push_back(87);    // Add another
scores.push_back(100);   // And another...

The vector automatically handles the memory for you — allocating more space when needed, keeping everything contiguous, and cleaning up when done. That’s why std::vector is the most-used container in all of C++, and usually your first choice when you need to store a collection of items.

Setting Up: Including the Header

To use std::vector, include the <vector> header:

#include <vector>
#include <iostream> // For std::cout

int main() {
    std::vector<int> numbers;
    return 0;
}

That’s it. No extra setup needed.

Declaring and Initializing a Vector

There are several ways to create a vector, depending on what you need.

Empty vector (add elements later)

std::vector<int> numbers;        // Empty, holds integers
std::vector<std::string> names;  // Empty, holds strings
std::vector<double> prices;      // Empty, holds doubles

Initialize with values

std::vector<int> scores = {95, 87, 100, 72, 88};

This creates a vector with 5 elements already inside. It’s the cleanest way to initialize a vector when you know the values upfront.

Initialize with a size and default value

std::vector<int> zeros(5);        // 5 elements, all 0 by default
std::vector<int> fives(5, 5);    // 5 elements, all 5
std::vector<std::string> empty_names(3, "Unknown"); // 3 elements, all "Unknown"

Copy from another vector

std::vector<int> original = {1, 2, 3};
std::vector<int> copy = original; // copy has {1, 2, 3}

The syntax: what does <int> mean?

std::vector<int> is a template. The type inside < > tells the vector what it holds. You can create a vector of any type:

std::vector<int>         // integers
std::vector<double>      // floating-point numbers
std::vector<char>        // characters
std::vector<std::string> // strings
std::vector<MyClass>     // your own custom classes

Adding and Removing Elements

Adding elements to the end: push_back()

push_back() adds an element to the end of the vector. It’s the most common way to build up a vector:

std::vector<int> scores;
scores.push_back(95);
scores.push_back(87);
scores.push_back(100);
// scores is now {95, 87, 100}

This is an O(1) amortized operation — very fast. (More on what this means in the memory section below.)

Adding with emplace_back() (modern C++)

emplace_back() is like push_back() but constructs the element in place — slightly more efficient for complex objects:

std::vector<std::string> names;
names.emplace_back("Alice");  // Preferred for strings and complex types
names.emplace_back("Bob");

For simple types like int, push_back and emplace_back are equivalent. For custom classes or strings, prefer emplace_back.

Inserting at a specific position

std::vector<int> numbers = {10, 20, 30, 40};

// Insert 15 at position 1 (second element)
numbers.insert(numbers.begin() + 1, 15);
// numbers is now {10, 15, 20, 30, 40}

Warning: Inserting in the middle is O(n) — the vector must shift all elements after the insertion point. Use this sparingly on large vectors.

Removing the last element: pop_back()

std::vector<int> scores = {95, 87, 100};
scores.pop_back(); // Removes 100
// scores is now {95, 87}

pop_back() is O(1) — fast.

Removing from a specific position: erase()

std::vector<int> numbers = {10, 20, 30, 40};
numbers.erase(numbers.begin() + 1); // Remove element at index 1
// numbers is now {10, 30, 40}

Like inserting, erasing from the middle is O(n) because elements must shift.

Removing all elements: clear()

std::vector<int> scores = {95, 87, 100};
scores.clear(); // Removes all elements
// scores is now empty: {}
// scores.size() == 0

Accessing Elements

Access by index with [ ]

std::vector<int> scores = {95, 87, 100, 72};

std::cout << scores[0]; // 95 (first element)
std::cout << scores[1]; // 87
std::cout << scores[3]; // 72 (last element)

This is O(1) — instant, regardless of vector size. Exactly like a regular array.

Important: operator[] does not check bounds. Accessing scores[10] on a 4-element vector is undefined behavior (crash or garbage). If you need safety, use at().

Safe access with at()

std::vector<int> scores = {95, 87, 100, 72};

try {
    std::cout << scores.at(1);  // 87 — safe, checks bounds
    std::cout << scores.at(10); // Throws std::out_of_range exception
} catch (const std::out_of_range& e) {
    std::cout << "Index out of range: " << e.what() << "\n";
}

Use at() during development and debugging; use [] in performance-critical code once you’ve verified correctness.

First and last elements: front() and back()

std::vector<int> scores = {95, 87, 100, 72};

std::cout << scores.front(); // 95 — first element
std::cout << scores.back();  // 72 — last element

Getting a raw pointer to the data: data()

Sometimes you need to pass the vector’s contents to a C-style function:

std::vector<int> numbers = {1, 2, 3, 4};
int* ptr = numbers.data(); // Raw pointer to the first element
// Now you can pass ptr to any C function expecting int*

Checking Size and Capacity

How many elements? size()

std::vector<int> scores = {95, 87, 100};
std::cout << scores.size(); // 3

Is the vector empty? empty()

std::vector<int> scores;

if (scores.empty()) {
    std::cout << "No scores yet!\n";
}

scores.push_back(95);

if (!scores.empty()) {
    std::cout << "We have scores.\n";
}

Always use empty() instead of size() == 0 — it’s more readable and equally fast.

Capacity vs. size

This is an important distinction many beginners miss:

std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

std::cout << v.size();     // 3 — three elements stored
std::cout << v.capacity(); // Often 4 — space for 4 elements allocated

When you add an element that exceeds capacity, the vector automatically allocates a larger block of memory (typically doubling) and copies everything over. This is what makes push_back O(1) amortized — usually instant, occasionally slow during reallocation.

Reserve capacity upfront: reserve()

If you know roughly how many elements you’ll add, use reserve() to avoid repeated reallocations:

std::vector<int> numbers;
numbers.reserve(1000); // Allocate space for 1000 elements now

for (int i = 0; i < 1000; i++) {
    numbers.push_back(i); // No reallocations — much faster
}

This is a significant performance optimization when adding large numbers of elements.

Shrink to fit: shrink_to_fit()

After removing many elements, the capacity stays high. Use shrink_to_fit() to release unused memory:

std::vector<int> numbers(1000, 0); // 1000 elements
numbers.clear();                    // 0 elements, but capacity still 1000
numbers.shrink_to_fit();           // Now capacity matches size (0)

Iterating Through a Vector

The cleanest way to iterate in modern C++:

std::vector<int> scores = {95, 87, 100, 72};

for (int score : scores) {
    std::cout << score << "\n";
}

To modify elements during iteration, use a reference:

std::vector<int> scores = {95, 87, 100, 72};

for (int& score : scores) {
    score += 5; // Add 5 to each score
}
// scores is now {100, 92, 105, 77}

Traditional index-based loop

Use this when you need the index:

std::vector<int> scores = {95, 87, 100, 72};

for (int i = 0; i < scores.size(); i++) {
    std::cout << "Score " << i << ": " << scores[i] << "\n";
}

Note: Use size_t or cast when comparing index to .size() to avoid signed/unsigned warnings:

for (size_t i = 0; i < scores.size(); i++) { ... }

Iterator-based loop

Iterators are objects that point to elements, similar to pointers. They’re used heavily in STL algorithms:

std::vector<int> scores = {95, 87, 100, 72};

for (auto it = scores.begin(); it != scores.end(); ++it) {
    std::cout << *it << "\n"; // Dereference iterator to get value
}

begin() points to the first element. end() points one-past-the-last element. You’ll use this form when working with STL algorithms.

Sorting and Searching

The STL’s <algorithm> header provides powerful functions that work seamlessly with vectors.

Sorting a vector

#include <vector>
#include <algorithm>

std::vector<int> scores = {72, 100, 87, 95, 88};
std::sort(scores.begin(), scores.end());
// scores is now {72, 87, 88, 95, 100}

Sort in descending order:

std::sort(scores.begin(), scores.end(), std::greater<int>());
// scores is now {100, 95, 88, 87, 72}

Sort with a custom comparator (sort structs by a field):

struct Student {
    std::string name;
    int grade;
};

std::vector<Student> students = {{"Alice", 92}, {"Bob", 85}, {"Carol", 97}};

// Sort by grade, lowest first
std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
    return a.grade < b.grade;
});

for (const auto& s : students) {
    std::cout << s.name << ": " << s.grade << "\n";
}
// Bob: 85
// Alice: 92
// Carol: 97

Finding an element: std::find()

#include <algorithm>

std::vector<int> scores = {95, 87, 100, 72};

auto it = std::find(scores.begin(), scores.end(), 100);

if (it != scores.end()) {
    std::cout << "Found 100 at index: " << (it - scores.begin()) << "\n";
} else {
    std::cout << "100 not found.\n";
}

Counting occurrences: std::count()

std::vector<int> rolls = {1, 6, 3, 6, 2, 6, 5};
int sixes = std::count(rolls.begin(), rolls.end(), 6);
std::cout << "Rolled a 6: " << sixes << " times\n"; // 3
std::vector<int> sorted = {10, 20, 30, 40, 50};

if (std::binary_search(sorted.begin(), sorted.end(), 30)) {
    std::cout << "30 is in the vector\n";
}

The vector must be sorted first for binary_search to work correctly.

Common Patterns and Real Examples

Example 1: Reading user input into a vector

#include <vector>
#include <iostream>

int main() {
    std::vector<int> scores;
    int input;

    std::cout << "Enter scores (enter -1 to stop):\n";

    while (std::cin >> input && input != -1) {
        scores.push_back(input);
    }

    std::cout << "You entered " << scores.size() << " scores.\n";

    // Calculate the average
    if (!scores.empty()) {
        int total = 0;
        for (int score : scores) {
            total += score;
        }
        double average = static_cast<double>(total) / scores.size();
        std::cout << "Average: " << average << "\n";
    }

    return 0;
}

Example 2: Filtering elements into a new vector

#include <vector>
#include <iostream>

int main() {
    std::vector<int> all_scores = {95, 43, 87, 55, 100, 62, 72, 38};
    std::vector<int> passing;       // scores >= 60
    std::vector<int> failing;       // scores < 60

    for (int score : all_scores) {
        if (score >= 60) {
            passing.push_back(score);
        } else {
            failing.push_back(score);
        }
    }

    std::cout << "Passing scores: ";
    for (int s : passing) std::cout << s << " ";

    std::cout << "\nFailing scores: ";
    for (int s : failing) std::cout << s << " ";

    return 0;
}
// Output:
// Passing scores: 95 87 100 62 72
// Failing scores: 43 55 38

Example 3: 2D vector (vector of vectors)

A vector of vectors works like a 2D grid or matrix:

#include <vector>
#include <iostream>

int main() {
    // Create a 3x4 grid initialized to 0
    int rows = 3, cols = 4;
    std::vector<std::vector<int>> grid(rows, std::vector<int>(cols, 0));

    // Set some values
    grid[0][0] = 1;
    grid[1][2] = 7;
    grid[2][3] = 5;

    // Print the grid
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            std::cout << grid[r][c] << " ";
        }
        std::cout << "\n";
    }

    return 0;
}
// Output:
// 1 0 0 0
// 0 0 7 0
// 0 0 0 5

Example 4: Removing duplicates

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> numbers = {4, 2, 7, 2, 4, 9, 7, 1};

    // Sort first — std::unique requires sorted input
    std::sort(numbers.begin(), numbers.end());

    // std::unique moves duplicates to the end; returns iterator to first duplicate
    auto last = std::unique(numbers.begin(), numbers.end());

    // Erase the duplicates
    numbers.erase(last, numbers.end());

    for (int n : numbers) std::cout << n << " ";
    // Output: 1 2 4 7 9

    return 0;
}

Vector vs. Array: When to Use Each

Featurestd::vectorC-style array
SizeDynamic (grows/shrinks)Fixed at compile time
Memory managementAutomaticManual (for heap arrays)
Bounds checkingVia .at()None
Works with STL algorithmsYesNeeds extra work
Pass to functionsEasy (by reference)Decays to pointer (loses size)
PerformanceNearly identicalSlightly faster in some edge cases
Modern C++ best practiceYes — default choiceAvoid unless required

Bottom line: Use std::vector by default. Only use raw arrays when you have a specific reason (e.g., interfacing with a C library, stack-allocated fixed-size data where you’re certain of the size, or performance-critical hot paths where profiling shows a difference).

Common Mistakes to Avoid

Mistake 1: Out-of-bounds access with [ ]

std::vector<int> v = {1, 2, 3};
std::cout << v[5]; // Undefined behavior — no error, just garbage or crash

// Fix: use .at() to get a proper error, or check size first
if (5 < v.size()) {
    std::cout << v[5];
}

Mistake 2: Modifying a vector while iterating over it

std::vector<int> nums = {1, 2, 3, 4, 5};

// WRONG: Adding to a vector while iterating invalidates iterators
for (auto it = nums.begin(); it != nums.end(); ++it) {
    if (*it == 3) {
        nums.push_back(10); // Undefined behavior!
    }
}

// FIX: Collect indices/values first, then modify
std::vector<int> to_add;
for (int n : nums) {
    if (n == 3) to_add.push_back(10);
}
for (int n : to_add) nums.push_back(n);

Mistake 3: Forgetting to reserve when building a large vector

// SLOW: may reallocate many times
std::vector<int> nums;
for (int i = 0; i < 1000000; i++) {
    nums.push_back(i);
}

// FAST: allocate once
std::vector<int> nums;
nums.reserve(1000000);
for (int i = 0; i < 1000000; i++) {
    nums.push_back(i);
}

Mistake 4: Using signed/unsigned comparison with size()

std::vector<int> v = {1, 2, 3};

// Warning: comparing int and size_t (unsigned)
for (int i = 0; i < v.size(); i++) { ... }

// Fix: use size_t or auto
for (size_t i = 0; i < v.size(); i++) { ... }

// Or better yet, use range-based for loop
for (int x : v) { ... }

Mistake 5: Calling front() or back() on an empty vector

std::vector<int> v;
int x = v.front(); // Undefined behavior — vector is empty!

// Fix: always check first
if (!v.empty()) {
    int x = v.front();
}

Quick Reference: Most Used Vector Operations

OperationCodeTime Complexity
Declare emptystd::vector<int> v;O(1)
Declare with valuesstd::vector<int> v = {1, 2, 3};O(n)
Add to endv.push_back(x);O(1) amortized
Remove from endv.pop_back();O(1)
Access by indexv[i] or v.at(i)O(1)
Get sizev.size()O(1)
Check if emptyv.empty()O(1)
First elementv.front()O(1)
Last elementv.back()O(1)
Clear allv.clear()O(n)
Insert at positionv.insert(v.begin() + i, x)O(n)
Remove at positionv.erase(v.begin() + i)O(n)
Sortstd::sort(v.begin(), v.end())O(n log n)
Find elementstd::find(v.begin(), v.end(), x)O(n)
Reserve capacityv.reserve(n)O(n)

Conclusion: You Now Know std::vector

std::vector is the workhorse of C++ programming. Once you’re comfortable with it, you’ll reach for it instinctively whenever you need a collection of items.

Here’s what you can now do:

The next step is practice. Try building a small project — a grade calculator, a to-do list, or a simple card game — using std::vector as your primary data structure. You’ll solidify your understanding fast.


Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. It covers vectors, pointers, OOP, and the entire language with the same intuitive, beginner-first approach used in this guide.

Just $19. 👉 Get the C++ Better Explained Ebook — $19


Edit page
Share this post on:

Next Post
C++ vs Python: Which Language Should You Learn First?