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
- Declare a C-style array (int a[5]) and read/write items by index
- Understand 0-based indexing and why the last index is size - 1
- Know that C-style arrays are fixed-size with NO bounds checking
- Use std::array as a safer, size-aware fixed array
- Grow a std::vector with push_back and read it with size, front, back
- Choose [] vs .at(), iterate with range-for, and build 2D vectors
💡 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: 87Your 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 193. 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: 100Now 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 safelyCommon Errors (and the fix)
- Out-of-bounds access (undefined behaviour): reading a[5] on a size-5 array touches memory you don't own. There's no error message — it just misbehaves. Stay within 0 to size - 1, and use .at() when an index might be wrong.
- Off-by-one in loops: for (int i = 0; i <= 5; i++) on a size-5 array runs one step too far and touches a[5]. Use i < size (strictly less-than), not <=.
- Modifying a vector while iterating it: calling push_back or erase inside a range-based for can reallocate the storage and invalidate your loop. Finish the loop first, then change the vector.
- [] hides mistakes that .at() would catch: v[i] never tells you the index was bad. If a value looks like garbage, swap to v.at(i) temporarily — it'll throw exactly where the bad index is.
- "error: 'vector' was not declared in this scope": you forgot #include <vector> (or <array> for std::array). Add the header at the top.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Declare array | Fixed size, no bounds check | |
| Declare std::array | #include <array> | |
| Declare vector | #include <vector> | |
| Add to end | v.push_back(4); | Vector only |
| Count | v.size() | Current element count |
| Read (fast) | v[i] | No bounds check |
| Read (safe) | v.at(i) | Throws if out of range |
| First / last | v.front() / v.back() | No index maths |
| Iterate | Range-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
- ✅ A C-style array int a[5] is fixed-size, 0-indexed, with no bounds checking
- ✅ The last valid index is size - 1; i < size in loops avoids off-by-one
- ✅ std::array<type, count> is a safer fixed array that knows its size()
- ✅ std::vector grows with push_back and reports its size()
- ✅ [] is fast but unchecked; .at() checks bounds and throws
- ✅ Use front/back, range-for, and vector<vector<int>> for grids — and prefer vector by default
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
- Previous: Lambda Expressions
- Next: Pointers & References — Understand memory addresses, pointers, and reference variables
- Quick reference: C++ cheat sheet
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.