Lesson 68 min read

Arrays & Vectors

Raw arrays are the motorcycle — fast but dangerous. Vectors are the car — fast, safe, and they handle the heavy lifting.

Two Ways to Store a List

Imagine you're moving into a new apartment. You could stack your books directly on the floor in a neat row — that's a C-style array. You know exactly how many books you have, the row never grows, and if you accidentally step past the end, you crush something.

Or you could buy a bookshelf with expandable sections — that's a std::vector. It starts with some room, and when you run out, it quietly gets a bigger shelf and moves everything over. You barely notice.

C-Style Arrays — The Old Guard

C++ inherited raw arrays from C. They're fast and sit directly on the stack, but they come with serious trade-offs:

  • Size is fixed at compile time
  • No bounds checking — go past the end and you silently corrupt memory
  • They decay to pointers when passed to functions, losing their size

For new code, you almost never need raw arrays. But you'll see them in legacy codebases and interview questions, so let's look at them briefly.

C-Style Array Basics

#include <iostream>
using namespace std;
int main() {
int scores[5] = {90, 85, 78, 92, 88};
// Access by index
cout << "First: " << scores[0] << endl;
cout << "Last: " << scores[4] << endl;
// Danger! No bounds checking
// scores[10] = 42; // Compiles fine, corrupts memory!
// Size trick (doesn't work if passed to a function)
int len = sizeof(scores) / sizeof(scores[0]);
cout << "Length: " << len << endl;
return 0;
}
Output
First: 90
Last:  88
Length: 5

std::array — The Modern Fixed-Size Array

C++11 introduced std::array as a safe wrapper around C-style arrays. It knows its own size, supports iterators, and works with STL algorithms — all with zero overhead compared to a raw array.

Use std::array when you know the size at compile time and it won't change.

std::array — Fixed Size, Full Safety

#include <iostream>
#include <array>
using namespace std;
int main() {
array<int, 4> temps = {72, 68, 75, 80};
cout << "Size: " << temps.size() << endl;
cout << "First: " << temps.front() << endl;
cout << "Last: " << temps.back() << endl;
// Safe access with bounds checking
try {
cout << temps.at(10) << endl; // Throws!
} catch (const out_of_range& e) {
cout << "Caught: " << e.what() << endl;
}
return 0;
}
Output
Size: 4
First: 72
Last:  80
Caught: array::at

std::vector — The Star of the Show

This is the container you'll use 90% of the time in C++. A vector is a dynamic array — it grows and shrinks as needed. Under the hood, it manages a chunk of heap memory and reallocates when it runs out of room.

Key Concepts: Size vs. Capacity

A vector has two numbers that matter:

  • Size — how many elements are currently stored
  • Capacity — how much memory is allocated (always ≥ size)

When you push_back and the size would exceed capacity, the vector allocates a new, larger block (typically 2x the old capacity), copies everything over, and frees the old block. This is why push_back is amortized O(1) — most calls are instant, but occasionally one triggers a reallocation.

Vector Operations — The Essentials

#include <iostream>
#include <vector>
using namespace std;
int main() {
// Create with initializer list
vector<int> nums = {10, 20, 30};
// Add elements
nums.push_back(40);
nums.push_back(50);
cout << "Size: " << nums.size() << endl;
cout << "Capacity: " << nums.capacity() << endl;
// Remove last element
nums.pop_back();
cout << "After pop_back, size: " << nums.size() << endl;
// Insert at position 1
nums.insert(nums.begin() + 1, 15);
// Print all elements
for (int n : nums) {
cout << n << " ";
}
cout << endl;
return 0;
}
Output
Size: 5
Capacity: 6
After pop_back, size: 4
10 15 20 30 40

at() vs [] — Safety vs Speed

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<string> names = {"Alice", "Bob", "Charlie"};
// [] is fast but no bounds checking
cout << names[0] << endl; // "Alice"
// names[99] = "Oops"; // Undefined behavior!
// at() checks bounds and throws if out of range
try {
cout << names.at(1) << endl; // "Bob"
cout << names.at(99) << endl; // Throws!
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Output
Alice
Bob
Error: vector::_M_range_check: __n (which is 99) >= this->size() (which is 3)
Note: Use vec.at(i) during development — it throws an exception on out-of-bounds access. vec[i] is faster but silently corrupts memory if you go out of bounds. Switch to [] only in performance-critical paths after you've verified correctness.

reserve() — Avoiding Unnecessary Reallocations

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
v.reserve(1000); // Pre-allocate space for 1000 elements
cout << "Size: " << v.size() << endl; // 0 — no elements yet
cout << "Capacity: " << v.capacity() << endl; // 1000 — room is ready
for (int i = 0; i < 1000; i++) {
v.push_back(i); // No reallocations happen!
}
cout << "Size after filling: " << v.size() << endl;
return 0;
}
Output
Size: 0
Capacity: 1000
Size after filling: 1000

2D Vectors — Vector of Vectors

#include <iostream>
#include <vector>
using namespace std;
int main() {
// Create a 3x4 grid initialized to 0
vector<vector<int>> grid(3, vector<int>(4, 0));
grid[0][0] = 1;
grid[1][2] = 5;
grid[2][3] = 9;
// Print the grid
for (const auto& row : grid) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
return 0;
}
Output
1 0 0 0
0 0 5 0
0 0 0 9

When to Use What?

  • std::vector — default choice. Use it unless you have a specific reason not to.
  • std::array — when the size is known at compile time and fixed (e.g., RGB color = 3 values).
  • C-style arrays — almost never in new code. Only when interfacing with C libraries or in very low-level code.

If you reserve() ahead of time, vectors match raw arrays in performance for sequential access. The compiler is smart enough to optimize away the overhead.

Challenge

Quick check

What happens when you access vec[100] on a vector with only 3 elements?
LoopsStrings