Chapter 12

Arrays & Vectors

Up until now, every variable we've worked with has held a single value. One int. One float. One std::string. That's fine when your program deals with one player, one score, one enemy. It falls apart the moment you need two of something. Or five. Or ten thousand.

This chapter is about collections — data structures that hold many values of the same type and let us work with them as a group. We'll meet the three main collection types in C++: raw arrays, std::array, and the one you'll use most often, std::vector. We'll learn how to put things in them, take things out, iterate over them, and avoid the handful of classic mistakes that collections invite.

In this chapter, we will:

Arrays

A raw array is the original C-style collection — a fixed-size block of memory holding a sequence of values of the same type. Declaring one looks like this:

int highScores[5];   // An array of 5 ints, values uninitialized

You can also declare and initialize in one go:

int highScores[5] = {100, 85, 70, 60, 45};

To access an individual element, use square brackets and an index:

std::cout << highScores[0] << std::endl;   // 100 — the first element
std::cout << highScores[4] << std::endl;   // 45  — the fifth element

highScores[0] = 150;                        // Overwrite the first score

Indexes run from 0 to n - 1. An array of size 5 has valid indexes 0 through 4.

Iterating Over an Array

int highScores[5] = {100, 85, 70, 60, 45};

for (int i = 0; i < 5; i++) {
    std::cout << "Rank " << (i + 1) << ": " << highScores[i] << std::endl;
}

The Problems With Raw Arrays

Raw arrays are simple and fast, but they have irritating limitations. They don't know their own size — once passed to a function, size information is gone. There's no bounds checking — accessing an out-of-range index causes undefined behavior. And the size must be known at compile time.

std::array

std::array is a thin wrapper around a raw array that remembers its size and plays nicely with the rest of the C++ standard library. It lives in the <array> header:

#include <array>

std::array<int, 5> highScores = {100, 85, 70, 60, 45};

The two things in the angle brackets are the element type and the size. Accessing elements works like a raw array, but std::array adds:

std::cout << highScores.size() << std::endl;   // 5 — it knows its own size!

std::cout << highScores.at(4)   << std::endl;   // 45 — bounds checked
std::cout << highScores.at(100) << std::endl;   // Throws — out of bounds

Use std::array over raw arrays in modern C++ whenever the size is known at compile time.

std::vector

std::vector is the most useful collection in C++. It's a dynamic array — one that can grow and shrink at runtime. Vectors live in the <vector> header:

#include <vector>

std::vector<int> scores;   // An empty vector of ints

Adding and Removing Elements

std::vector<int> scores;

scores.push_back(100);
scores.push_back(85);
scores.push_back(70);

std::cout << scores.size() << std::endl;   // 3

scores.pop_back();                          // Removes the 70
scores.clear();                             // Remove all elements
bool isEmpty = scores.empty();              // true if size is 0

Initializing a Vector

std::vector<int> scores = {100, 85, 70, 60, 45};

std::vector<int> zeros(10, 0);   // 10 elements, all zero
std::vector<int> tenItems(10);   // 10 elements, default-initialized

Accessing Elements

std::vector<int> scores = {100, 85, 70};

std::cout << scores[0]     << std::endl;   // 100 — fast, no bounds check
std::cout << scores.at(2)  << std::endl;   // 70  — bounds checked

Iterating Over a Vector

// Traditional indexed loop:
for (int i = 0; i < scores.size(); i++) {
    std::cout << scores[i] << std::endl;
}

// Range-based for (preferred):
for (int score : scores) {
    std::cout << score << std::endl;
}

// Modify each element via reference:
for (int& score : scores) {
    score = score + 10;   // Bonus 10 points for every score
}

2D Arrays and Vectors

Games are full of grids — tile maps, inventory screens, level layouts. We can represent these using nested collections:

std::vector<std::vector<int>> tileMap = {
    {1, 1, 1, 1, 1},
    {1, 0, 0, 0, 1},
    {1, 0, 0, 0, 1},
    {1, 0, 0, 0, 1},
    {1, 1, 1, 1, 1}
};

int centerTile = tileMap[2][2];   // 0 — the center of the room
int cornerTile = tileMap[0][0];   // 1 — the top-left corner

for (int row = 0; row < tileMap.size(); row++) {
    for (int col = 0; col < tileMap[row].size(); col++) {
        if (tileMap[row][col] == 1) {
            std::cout << "#";
        } else {
            std::cout << ".";
        }
    }
    std::cout << std::endl;
}

Iterators

An iterator is an object that points into a collection — a more general cousin of a pointer. The two methods begin() and end() return iterators. Here's an explicit iterator loop:

std::vector<int> scores = {100, 85, 70};

for (auto it = scores.begin(); it != scores.end(); ++it) {
    std::cout << *it << std::endl;
}

The range-based for loop is just syntactic sugar for exactly this iterator loop. The compiler rewrites for (int score : scores) into the begin()/end() form above.

Common Vector Pitfalls

Out-of-bounds access. Accessing scores[10] when the vector has five elements is not caught by [] — you just read invalid memory. Use at() during development.

Undefined behavior means the language specification doesn't say what should happen. Your program might crash, produce wrong results, or appear to work today and fail silently tomorrow. When you see the term, treat it as a flashing red light.

Iterator invalidation. When a vector grows and reallocates, all iterators, pointers, and references into it become invalid.

std::vector<int> scores = {100, 85, 70};
int& first = scores[0];    // Reference to the first element
scores.push_back(50);       // Vector might reallocate — first could be invalid!
first = 200;               // Dangerous — do not do this

Iterating Safely While Modifying

You can't simply remove an element from a vector while a range-based for is iterating over it. Here are the three safe patterns:

Pattern 1 — iterate backwards:

for (int i = enemies.size() - 1; i >= 0; i--) {
    if (enemies[i] == 0) {
        enemies.erase(enemies.begin() + i);
    }
}

Pattern 2 — swap and pop (fast, but changes order):

for (int i = 0; i < enemies.size(); ) {
    if (enemies[i] == 0) {
        enemies[i] = enemies.back();
        enemies.pop_back();
    } else {
        i++;
    }
}

Pattern 3 — the erase-remove idiom (clean, standard):

enemies.erase(
    std::remove(enemies.begin(), enemies.end(), 0),
    enemies.end()
);

std::remove shuffles all the non-zero elements to the front and returns an iterator pointing to the first leftover. Then erase chops off everything from that iterator to the end.

reserve() and capacity()

A vector has a size (how many elements it currently holds) and a capacity (how many it could hold before needing to reallocate). If you know up-front how many elements you'll add, you can skip the reallocation dance entirely with reserve():

std::vector<int> scores;
scores.reserve(1000);   // Allocate room for 1000 elements up front

for (int i = 0; i < 1000; i++) {
    scores.push_back(i);
}
// No reallocations occurred

Moving vs Copying (Brief)

When a vector grows and reallocates, it needs to get each existing element into the new block. Copying duplicates the contents; moving transfers ownership without duplication. You can explicitly request a move with std::move:

std::string name = "Ada Lovelace";
std::vector<std::string> names;

names.push_back(std::move(name));   // Move name into the vector
// name is now in an unspecified but valid (usually empty) state.

AI Exercise

Open your AI chatbot and paste in this prompt:

"I'm a C++ beginner. I've just learned about std::vector, iterators, and the pitfalls of modifying a vector while iterating over it. Please write a small program that starts with a std::vector<int> holding the values {10, 0, 25, 0, 15, 0, 30}. Write a function that removes every zero from the vector. Show me three different implementations of this function: (1) iterating backwards by index, (2) the swap-and-pop pattern, and (3) the erase-remove idiom. For each, explain briefly what it's doing, what its performance characteristics are, and when you would prefer it over the others. Finish with a one-paragraph recommendation for which you would reach for first in real code, and why."

Summary

You now have the two most important collection types in C++ in your toolkit. std::array for fixed-size collections, and std::vector for collections that grow and shrink. You've seen how to build 2D structures for grids and tile maps. You understand what iterators are and what's really going on inside a range-based for loop. You've met the three classic patterns for removing elements while iterating, plus reserve() for skipping reallocation costs.

Vectors in particular will turn up in almost every game you write from this point on. Inventories, enemy lists, bullets, tiles, particles — they're all vectors. Next up is a project where vectors do real work in a real game.