Arrays & Vectors

Reviewed & published by Brayan K

By the end of this lesson you'll be able to store a whole list of values in one variable, read and change any item by its index, and grow a collection on demand with std::vector<int> — the container you'll reach for in almost every C++ program you write.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A C-style array is a fixed row of post-office boxes. You decide how many boxes to build, and that number can never change. Each box has a number on it starting at 0, and you reach a box by its number — scores[0], scores[1], and so on. There's no security guard: if you ask for box number 5 on a 5-box wall (boxes 0–4), nobody stops you — you just grab whatever is in the next bit of wall, which is a bug.

A std::vector is the same wall of boxes, but magic and expandable: you can add a new box on the end any time with push_back, it always knows how many boxes it has with size(), and its .at() door has a guard that refuses an invalid box number. That's why modern C++ reaches for vector first.

1. C-Style Arrays

An array stores many values of one type in a single named block. You declare it as type name[count], and you reach each value by its index — an offset that starts at 0. So int a[5] holds five ints at indexes 0, 1, 2, 3, and 4; the last valid index is always count - 1. Two things to burn in now: the size is fixed (you can't grow it), and there is no bounds checking (reading past the end is undefined behaviour, not an error). Read this worked example, run it, then you'll write your own.

#include <iostream>
using namespace std;

int main() {
    // A C-style array is a FIXED row of boxes, all the same type.
    // int scores[5] => 5 ints, indexes 0,1,2,3,4 (NOT 1..5).
    int scores[5] = {95, 87, 73, 100, 68};

    // Indexing reads/writes one box. The index is an OFFSET from the start.
    cout << "First: " << scores[0] << endl;   // First: 95   (offset 0)
    cout << "Last:  " << scores[4] << endl;   // Last:  68   (offset 4 = size-1)

    scores[2] = 85;                           // overwrite box 2 (was 73)
    cout << "Changed scores[2] to " << scores[2] << endl; // 85

    // The size is FIXED — you can't push_back or grow a C-style array.
    // There is NO bounds checking: scores[5] would be undefined behaviour.

    // Sum every box with a counting loop (note i < 5, not i <= 5).
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += scores[i];                     // 95+87+85+100+68
    }
    cout << "Sum: " << sum << endl;           // Sum: 435

    double average = static_cast<double>(sum) / 5;  // cast so we keep decimals
    cout << "Average: " << average << endl;   // Average: 87
    return 0;
}

// ✅ Expected output:
//    First: 95
//    Last:  68
//    Changed scores[2] to 85
//    Sum: 435
//    Average: 87

Your turn. The program below is almost complete — fill in the three blanks marked ___ using the hints, then run it. Watch the indexes carefully.

#include <iostream>
using namespace std;

int main() {
    // 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".

    // 1) Make an int array called "ages" holding 5, 8, 12, 16, 21
    int ages[5] = ___;        // 👉 a list in { } e.g. {5, 8, 12, 16, 21}

    // 2) Print the FIRST element (remember: offset 0, not 1)
    cout << "Youngest: " << ages[___] << endl;   // 👉 the first index

    // 3) Print the LAST element of a size-5 array
    cout << "Oldest: " << ages[___] << endl;     // 👉 size - 1

    // ✅ Expected output:
    //    Youngest: 5
    //    Oldest: 21
    return 0;
}

2. std::array — a Safer Fixed Array

A C-style array forgets its own length, so you end up passing a separate size around by hand. std::array<type, count> fixes that: it's still fixed-size and lives on the stack like a raw array, but it knows its size() and offers a bounds-checked .at(). You need #include <array> to use it. Reach for it when the count is known up front and never changes.

#include <iostream>
#include <array>          // needed for std::array
using namespace std;

int main() {
    // std::array is a SAFER fixed-size array. Size is part of its type:
    //   array<type, count>
    array<int, 4> temps = {18, 21, 24, 19};

    // It knows its own size — no separate counter to keep in sync.
    cout << "How many: " << temps.size() << endl;   // How many: 4

    // [] works like a raw array (no bounds check).
    cout << "temps[0]: " << temps[0] << endl;       // temps[0]: 18

    // .at() adds bounds checking — throws if the index is invalid.
    cout << "temps.at(3): " << temps.at(3) << endl; // temps.at(3): 19

    // Range-based for: read each element without managing an index.
    cout << "All temps: ";
    for (int t : temps) {
        cout << t << " ";                            // 18 21 24 19
    }
    cout << endl;
    return 0;
}

// ✅ Expected output:
//    How many: 4
//    temps[0]: 18
//    temps.at(3): 19
//    All temps: 18 21 24 19

3. std::vector — the One You'll Use Most

A vector is a resizable array. You add to the end with push_back, ask how big it is with size(), and read items with [] (fast, unchecked) or .at() (checked — it throws if the index is invalid). front() and back() grab the first and last item, and a range-based for visits every element without you managing an index. You need #include <vector>. For almost every list of data, this is the container to use.

#include <iostream>
#include <vector>         // needed for std::vector
using namespace std;

int main() {
    // A vector is a RESIZABLE array — the workhorse of modern C++.
    vector<int> numbers = {10, 20, 30};   // start with three values
    vector<string> fruits;                // start empty

    // push_back ADDS an element on the end and grows the vector.
    fruits.push_back("Apple");
    fruits.push_back("Banana");
    fruits.push_back("Cherry");

    // size() tells you how many elements there are right now.
    cout << "Fruits: " << fruits.size() << endl;   // Fruits: 3

    // [] is fast but UNCHECKED. .at() is CHECKED (throws if out of range).
    cout << "First (unchecked): " << fruits[0] << endl;   // Apple
    cout << "Second (checked):  " << fruits.at(1) << endl; // Banana

    // front() and back() grab the first and last without indexing maths.
    cout << "front(): " << fruits.front() << endl; // Apple
    cout << "back():  " << fruits.back() << endl;  // Cherry

    // Range-based for is the cleanest way to visit every element.
    // 'const string &' = read-only, no copy made.
    cout << "All fruits:" << endl;
    for (const string &fruit : fruits) {
        cout << "- " << fruit << endl;             // - Apple / - Banana / - Cherry
    }

    // Grow numbers too, then total it up.
    numbers.push_back(40);                          // now {10,20,30,40}
    int sum = 0;
    for (int n : numbers) sum += n;
    cout << "Sum of numbers: " << sum << endl;      // Sum of numbers: 100
    return 0;
}

// ✅ Expected output:
//    Fruits: 3
//    First (unchecked): Apple
//    Second (checked):  Banana
//    front(): Apple
//    back():  Cherry
//    All fruits:
//    - Apple
//    - Banana
//    - Cherry
//    Sum of numbers: 100

Now you build one. Start from an empty vector and add items to it, then read it back. Fill in the blanks:

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

int main() {
    // 🎯 YOUR TURN — build a shopping list with a vector.
    vector<string> cart;     // starts empty

    // 1) Add "Milk" to the cart
    cart.___("Milk");        // 👉 the method that adds to the end

    // 2) Add "Bread" to the cart
    cart.push_back(___);     // 👉 the item, in "double quotes"

    // 3) Print how many items are in the cart
    cout << "Items: " << cart.___() << endl;   // 👉 the method that returns the count

    // 4) Print the last item using back()
    cout << "Last added: " << cart.back() << endl;

    // ✅ Expected output:
    //    Items: 2
    //    Last added: Bread
    return 0;
}

4. 2D Vectors (a Grid)

Sometimes data is a grid — a board, a spreadsheet, an image. A vector<vector<int>> is a vector whose elements are themselves vectors, so you index it with two brackets: grid[row][col]. The outer size() gives the number of rows, and grid[row].size() gives the columns in that row. A nested for visits every cell.

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

int main() {
    // A 2D vector is a vector OF vectors — a grid of rows and columns.
    vector<vector<int>> grid = {
        {1, 2, 3},     // row 0
        {4, 5, 6},     // row 1
        {7, 8, 9}      // row 2
    };

    // grid[r][c]: first pick the row, then the column.
    cout << "grid[1][2] = " << grid[1][2] << endl;   // grid[1][2] = 6

    // grid.size() = number of rows; grid[r].size() = columns in that row.
    cout << "Rows: " << grid.size()
         << ", Cols: " << grid[0].size() << endl;    // Rows: 3, Cols: 3

    // Visit every cell with a nested range-based for.
    int total = 0;
    for (const vector<int> &row : grid) {
        for (int cell : row) {
            total += cell;                           // add up 1..9
        }
    }
    cout << "Sum of grid: " << total << endl;        // Sum of grid: 45
    return 0;
}

// ✅ Expected output:
//    grid[1][2] = 6
//    Rows: 3, Cols: 3
//    Sum of grid: 45

🔎 Deep Dive: [] vs .at()

v[i] trusts you completely — it does no bounds checking. If i is out of range you get undefined behaviour: maybe a crash, maybe garbage, maybe a silent corruption that bites you ten lines later. It's the fast path for indexes you've already validated (like a clean for loop).

v.at(i) does the same read but checks the index first and throws a std::out_of_range exception if it's invalid. Use it whenever the index could be wrong — for example, an index that came from user input. A loud, immediate error beats a mysterious one.

vector<int> v = {10, 20, 30};
v[5];     // ⚠️ undefined behaviour — no warning, no error
v.at(5);  // ✅ throws std::out_of_range — fails loudly and safely

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Declare arrayFixed size, no bounds check
Declare std::array#include <array>
Declare vector#include <vector>
Add to endv.push_back(4);Vector only
Countv.size()Current element count
Read (fast)v[i]No bounds check
Read (safe)v.at(i)Throws if out of range
First / lastv.front() / v.back()No index maths
IterateRange-based for

Mini-Challenge: Top Score & Total

No blanks this time — just a brief and an outline to keep you on track. Build it, run it, and check your output against the example in the comments. Finding the max and the sum of a list is a pattern you'll use constantly.

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

int main() {
    // 🎯 MINI-CHALLENGE: highest score & total
    // 1. Make a vector<int> called "scores" holding {72, 95, 60, 88, 95}.
    // 2. Loop over it to find the LARGEST value (start max at scores[0],
    //    then compare each element and keep the bigger one).
    // 3. Also add every value up into an int "total".
    // 4. Print:  "Top score: X"  then  "Total: Y"
    //
    // Hints: use a range-based for (for (int s : scores) { ... }).
    //        size() is handy if you want the count too.
    //
    // ✅ Expected output:
    //    Top score: 95
    //    Total: 410

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

What is the index of the FIRST element in a C-style array?

  • 1
  • 0
  • -1
  • It depends on the type

Answer: 0. Array indexing starts at 0; the first element sits 0 steps from the start.

For an array of size 5, what is the last valid index?

  • 5
  • 6
  • 4
  • 0

Answer: 4. Valid indexes are 0..size-1, so the last is 4 for a size-5 array.

What is true about a C-style array's size?

  • It grows automatically
  • It is fixed and cannot grow
  • It is always 10
  • It changes with push_back

Answer: It is fixed and cannot grow. C-style arrays are fixed-size; you cannot push_back or grow them.

Which container is the resizable, grow-on-demand array of modern C++?

  • std::array
  • C-style array
  • std::vector
  • std::list only

Answer: std::vector. std::vector resizes on demand and is the go-to container in modern C++.

Which method adds an element to the end of a std::vector?

  • append()
  • push_back()
  • add()
  • insert_end()

Answer: push_back(). push_back() adds an element to the end and grows the vector.

What is the difference between v[i] and v.at(i)?

  • They are identical
  • v[i] is bounds-checked, v.at(i) is not
  • v.at(i) is bounds-checked and throws; v[i] is not
  • v[i] returns a copy, v.at(i) a reference

Answer: v.at(i) is bounds-checked and throws; v[i] is not. v.at(i) checks bounds and throws std::out_of_range; v[i] does no checking.

Which header must you include to use std::vector?

  • <array>
  • <vector>
  • <list>
  • <collection>

Answer: <vector>. std::vector requires #include <vector>.

How do you index a 2D vector named grid at row 1, column 2?

  • grid(1,2)
  • grid[1,2]
  • grid[1][2]
  • grid.at(1,2)

Answer: grid[1][2]. A vector of vectors is indexed with two brackets: grid[row][col].

What does reading a[5] on a size-5 array cause?

  • A compile error
  • Undefined behavior
  • It returns 0
  • It throws std::out_of_range

Answer: Undefined behavior. Index 5 is out of range on a size-5 array; C-style [] does no bounds checking, so it is undefined behavior.

What does grid.size() return for a 2D vector?

  • The total number of cells
  • The number of rows
  • The number of columns
  • Always 1

Answer: The number of rows. The outer size() gives the number of rows; grid[r].size() gives that row's columns.

Continue this course

Frequently asked questions

Should I use a C-style array, std::array, or std::vector?

Reach for std::vector by default — it grows on demand, knows its own size, and is safe to pass around. Use std::array only when the size is fixed and known at compile time and you want stack storage. Use a raw C-style array (int a[5]) mostly for learning and for talking to old C code; in modern C++ you rarely need one.

What is the difference between v[i] and v.at(i)?

Both read element i. v[i] does NOT check bounds — if i is out of range you get undefined behaviour (a crash, garbage, or silent corruption). v.at(i) DOES check bounds and throws a std::out_of_range exception instead. Use [] in tight loops where you have already checked the index, and .at() when the index might be invalid.

Why does my array index start at 0?

The index is an offset from the start, not a count. The first element sits 0 steps from the beginning, so it is a[0]. An array of size 5 therefore has valid indexes 0, 1, 2, 3, 4 — the last one is size minus 1, never size itself. Writing a[5] on a size-5 array is the classic off-by-one bug.

Why shouldn't I add or remove elements while looping over a vector?

Calling push_back or erase can move the vector's storage to a new memory location, which invalidates any references, pointers, or iterators you are using to loop. The fix is to finish iterating first, or collect the changes and apply them after the loop, or loop by index with care.

Does std::vector slow my program down compared to a raw array?

For element access, no — v[i] compiles down to the same pointer arithmetic as a raw array. The only cost is when the vector grows and has to reallocate. If you know the final size, call v.reserve(n) up front to do all the allocation once. For the vast majority of programs, vector is both fast and far safer.

Related lessons