What you'll learn
C++ offers several sequence types because fixed-size storage, growable ownership, and non-owning access are different jobs. This lesson builds a safe selection rule aroundstd::array, std::vector, and std::span.
By the end, you'll be able to:
- Choose a sequence type from its ownership and size contract
- Create, inspect, and modify std::array and std::vector values
- Predict when vector growth invalidates pointers, references, and iterators
- Accept contiguous sequences without taking ownership by using std::span
Sequence ownership model
Begin with two questions: who owns the elements, and can the number of elements change? A built-in array owns a fixed number of elements but has awkward value behavior. std::array keeps that fixed size while behaving like a regular value.std::vector owns a runtime-sized contiguous sequence. std::spanmerely views contiguous elements owned elsewhere.
| Type | Owns elements? | Size | Best fit |
|---|---|---|---|
T[N] | Yes | Compile time | Low-level interop or language mechanics |
std::array<T, N> | Yes | Compile time | Fixed-size value |
std::vector<T> | Yes | Runtime, growable | Default dynamic sequence |
std::span<T> | No | Runtime view | Borrowed function parameter |
Key idea
Fixed-size arrays
A std::array stores its elements directly inside the object. Its size is part of its type, so it cannot grow, but it can be copied, assigned, returned, compared, and used with standard algorithms. Use .at() when an index is untrusted because it checks the boundary and throws on failure.
#include <algorithm>
#include <array>
#include <iostream>
int main() {
std::array<int, 5> scores{72, 91, 84, 68, 95};
std::ranges::sort(scores);
for (int score : scores) {
std::cout << score << ' ';
}
std::cout << "\ncount: " << scores.size() << '\n';
}Watch out
operator[] does not perform a bounds check. An out-of-range access has undefined behavior; it is not guaranteed to produce a useful exception or crash.Dynamic sequences with vector
std::vector owns a contiguous block and manages its lifetime automatically. Its size is the number of live elements; its capacity is the number that can fit before another allocation is needed. push_back copies or moves an existing value, while emplace_back constructs an element from arguments.
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> readings{18, 21, 20};
readings.push_back(24);
readings.insert(readings.begin() + 1, 19);
const int total{std::accumulate(readings.begin(), readings.end(), 0)};
std::cout << "samples: " << readings.size() << '\n';
std::cout << "total: " << total << '\n';
}front()andback()require a non-empty vectorclear()destroys elements but normally retains capacityreserve(n)grows capacity without creating elementsresize(n)changes the number of live elements
Growth and invalidation
When growth exceeds capacity, a vector allocates a larger block and moves or copies its elements. Every pointer, reference, iterator, and span into the old block then becomes invalid. Even without reallocation, insertion or erasure can invalidate positions at and after the changed point.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values;
values.reserve(3);
for (int value : {10, 20, 30}) {
values.push_back(value);
std::cout << values.size() << '/' << values.capacity() << '\n';
}
}Reserve when the estimate is meaningful
Borrowed views with span
A std::span<T> contains a pointer and a count. It can view a built-in array,std::array, or std::vector without copying. A span does not extend the lifetime of its elements, so the owner must remain alive and must not invalidate its storage.
#include <array>
#include <iostream>
#include <span>
#include <vector>
int sum(std::span<const int> values) {
int total{};
for (int value : values) total += value;
return total;
}
int main() {
std::array fixed{1, 2, 3};
std::vector dynamic{4, 5, 6};
std::cout << sum(fixed) << ' ' << sum(dynamic) << '\n';
}Key idea
std::span<const T> for a read-only borrowed range andstd::span<T> only when mutation through the view is part of the contract.Multidimensional collections
A rectangular compile-time matrix can be an array of arrays. A runtime matrix is often a single vector with row-major indexing: row * columns + column. Flat storage makes shape explicit, improves locality, and avoids one allocation per row.
#include <iostream>
#include <vector>
int main() {
const std::size_t rows{2};
const std::size_t columns{3};
std::vector<int> matrix{1, 2, 3, 4, 5, 6};
for (std::size_t row{}; row < rows; ++row) {
for (std::size_t column{}; column < columns; ++column) {
std::cout << matrix.at(row * columns + column) << ' ';
}
std::cout << '\n';
}
}Recap & quick check
Key takeaways
- std::array is a fixed-size value; std::vector is the default growable owning sequence.
- Size counts live elements, while capacity describes currently allocated room.
- Vector reallocation invalidates every pointer, reference, iterator, and span into its old storage.
- std::span is a non-owning contiguous view and never extends the owner's lifetime.
- Flat row-major storage is often a clear and efficient runtime matrix representation.
Quick check
1. Which type best owns a runtime-sized contiguous sequence?
2. What does vector::reserve change?
3. When vector reallocates, what happens to an existing pointer into it?
4. What ownership does std::span have?
Next: Module 8 — Pointers, Lifetimes & Dynamic Memory, where those borrowed addresses become an explicit part of the design.