05 · Arrays & std::vector Basics¶
C++ gives you two main ways to store a sequence of values: fixed-size
C-style arrays, and the dynamically-sized std::vector.
C-style arrays¶
#include <iostream>
int main() {
int scores[5] = {90, 85, 77, 92, 88};
std::cout << scores[0] << std::endl; // 90 -- indexing starts at 0
std::cout << scores[4] << std::endl; // 88 -- last valid index is size - 1
scores[2] = 100; // arrays are mutable
std::cout << scores[2] << std::endl; // 100
int size = sizeof(scores) / sizeof(scores[0]);
std::cout << "size: " << size << std::endl; // 5
}
C-style arrays have a fixed size decided at compile time and don't know
their own length — you have to track it yourself (or compute it with the
sizeof trick above, which only works for arrays, not pointers). There's also
no bounds checking: scores[10] compiles and may silently corrupt memory
or crash. For these reasons, modern C++ code reaches for std::vector almost
everywhere a raw array might have been used in C.
std::vector — a dynamic, resizable array¶
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers; // starts empty
numbers.push_back(10); // append an element
numbers.push_back(20);
numbers.push_back(30);
std::cout << numbers.size() << std::endl; // 3
std::cout << numbers[0] << std::endl; // 10
std::cout << numbers.at(1) << std::endl; // 20
numbers[1] = 25; // modify in place
std::cout << numbers[1] << std::endl; // 25
}
std::vector<int> declares a vector holding int elements — the type in
angle brackets is the element type. push_back grows the vector as needed,
so you never have to decide a size up front.
operator[] vs .at()¶
std::vector<int> v = {1, 2, 3};
std::cout << v[10] << std::endl; // undefined behavior -- no bounds check, may crash or return garbage
// std::cout << v.at(10) << std::endl; // throws std::out_of_range -- safe, checked
operator[] is fast but unchecked; .at() is slightly slower but throws a
catchable exception on an invalid index. Prefer .at() when indices come
from outside input, and [] in tight loops where you've already validated
the range. Exceptions are covered in Module 9.
Initializing a vector with values¶
std::vector<std::string> names = {"Alice", "Bob", "Carol"};
std::vector<double> prices(5, 0.0); // 5 elements, each initialized to 0.0
std::cout << names.size() << std::endl; // 3
std::cout << prices.size() << std::endl; // 5
Iterating over a vector¶
std::vector<int> temps = {68, 72, 75, 70, 66};
// Index-based
for (size_t i = 0; i < temps.size(); i++) {
std::cout << temps[i] << " ";
}
std::cout << std::endl;
// Range-based (preferred when you don't need the index)
for (int t : temps) {
std::cout << t << " ";
}
std::cout << std::endl;
// 68 72 75 70 66 (both loops)
size() returns a size_t (an unsigned integer type), so the loop counter
i should be size_t too — comparing a signed int against an unsigned
size_t can trigger compiler warnings and subtle bugs with negative values.
Common vector operations¶
std::vector<int> v = {5, 3, 8, 1};
v.push_back(9); // {5, 3, 8, 1, 9}
v.pop_back(); // {5, 3, 8, 1} -- removes the last element
std::cout << v.empty() << std::endl; // 0 (false) -- not empty
std::cout << v.front() << std::endl; // 5 -- first element
std::cout << v.back() << std::endl; // 1 -- last element
v.clear(); // removes all elements
std::cout << v.empty() << std::endl; // 1 (true)
A 2D grid with a vector of vectors¶
std::vector<std::vector<int>> grid = {
{1, 2, 3},
{4, 5, 6}
};
std::cout << grid[1][2] << std::endl; // 6 -- row 1, column 2
for (const auto& row : grid) {
for (int cell : row) {
std::cout << cell << " ";
}
std::cout << std::endl;
}
// 1 2 3
// 4 5 6
How It Actually Works¶
A C-style array int arr[5]; declared inside a function is just five
contiguous int-sized slots carved out of the current stack frame — no
allocator involved, no bookkeeping stored anywhere near it. arr[2] doesn't
"look up" an element; it computes an address — arr's base address plus
2 * sizeof(int) — and reads/writes memory there directly. That's why
arrays don't know their own length at runtime: the compiler only tracks the
size while compiling (sizeof(arr) works because the compiler still has the
type), but once the array decays to a raw pointer (e.g. when passed to a
function), that information is gone, and out-of-bounds access silently reads
or corrupts whatever memory happens to sit past the array — undefined
behavior with no bounds check to catch it.
std::vector<int> is a small, fixed-size object (typically three pointers:
begin, end, and end-of-capacity) that sits wherever you declare it, but it
owns a separate block of heap memory it allocates with new[] under the
hood for the actual elements. push_back checks if there's spare capacity;
if not, it allocates a new, larger block (commonly doubling the previous
capacity), move-or-copy-constructs every existing element into it, and frees
the old block — an operation that is O(n) on the rare occasions it happens,
but averages out to amortized O(1) per push because doubling means it
happens exponentially less often as the vector grows. This is also why
inserting a lot of elements is faster when you reserve() capacity
up-front: it eliminates the repeated reallocate-and-move cycles. And it's
why a pointer or iterator into a vector can be silently invalidated by a
push_back — the whole backing block may have moved to a new address.
🔀 See this in another language¶
Exercise¶
Write a program that builds a std::vector<int> of the squares of the
numbers 1 through 10 (using push_back in a loop), then prints the vector's
size(), its sum (using a range-based for loop), and its largest element
(track a running maximum as you iterate). Use .at() when reading the first
and last elements.