Store and Access Multiple Values with Arrays and Vectors
Learn to store lists of values using C++ arrays and std::vector, iterate over them, and avoid the most common beginner mistakes.
By the end of this page, you will be able to declare and use both raw C++ arrays and std::vector, iterate over a collection of values, grow a list at runtime with push_back, and recognize the mistakes that cause programs to crash or produce garbage output.
What and Why
Imagine tracking the scores of ten players in a game. Without collections, you might write:
int score0 = 91;
int score1 = 78;
int score2 = 85;
// ...seven more variablesThis becomes unmanageable immediately. Collections let you store many values under a single name and refer to each one by its index β its numeric position in the list.
C++ gives you two primary tools:
- Raw arrays β a fixed-size block of memory whose length you must know at compile time.
std::vectorβ a resizable list from the C++ Standard Library that grows and shrinks as needed.
For most new code, you will reach for std::vector. Raw arrays still appear everywhere in older codebases, embedded systems, and C library interfaces, so understanding both matters.
Step by Step
Raw Arrays
A raw array stores a fixed number of values of the same type. You declare it with the type, a name, and the count in square brackets.
#include <iostream>
int main() {
int scores[5] = {91, 78, 85, 62, 99};
std::cout << scores[0] << "\n"; // first element: 91
std::cout << scores[4] << "\n"; // last element: 99
return 0;
}Three rules to memorize now:
- Indices start at 0, not 1.
- The last valid index is always size β 1. For five elements, that is 4.
- The size must be a constant known at compile time.
To visit every element, use a loop. The classic form uses the array size as the stopping condition:
#include <iostream>
int main() {
int scores[5] = {91, 78, 85, 62, 99};
for (int i = 0; i < 5; ++i) {
std::cout << "Score " << i << ": " << scores[i] << "\n";
}
return 0;
}C++11 introduced the range-based for loop, which is cleaner when you do not need the index:
#include <iostream>
int main() {
int scores[5] = {91, 78, 85, 62, 99};
for (int s : scores) {
std::cout << s << "\n";
}
return 0;
}std::vector
std::vector lives in the <vector> header. It behaves like an array that can grow. You do not need to know the size upfront.
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores; // empty vector of ints
scores.push_back(91); // add elements one by one
scores.push_back(78);
scores.push_back(85);
std::cout << "Count: " << scores.size() << "\n"; // 3
std::cout << "First: " << scores[0] << "\n"; // 91
return 0;
}You can also initialize a vector with values directly, just like an array:
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {91, 78, 85, 62, 99};
for (int s : scores) {
std::cout << s << "\n";
}
return 0;
}When you know the size upfront but not the values, use the parenthesis constructor to create a vector pre-filled with zeros (or default values):
#include <vector>
int main() {
std::vector<int> scores(10); // 10 elements, all initialized to 0
std::vector<double> temps(365); // 365 elements, all 0.0
}
{10}vs(10):std::vector<int> v{10}creates a vector with one element whose value is 10.std::vector<int> v(10)creates a vector with ten elements all set to 0. The braces versus parentheses distinction matters here.
To remove the last element, call pop_back():
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {91, 78, 85};
scores.pop_back(); // removes 85
std::cout << "Now has " << scores.size() << " elements\n"; // 2
return 0;
}Common Patterns
Building a list from user input
One of the most common uses of std::vector is collecting an unknown number of values at runtime:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
int input;
std::cout << "Enter numbers (0 to stop):\n";
while (std::cin >> input && input != 0) {
numbers.push_back(input);
}
std::cout << "You entered " << numbers.size() << " numbers.\n";
return 0;
}Computing a sum or average
A running total over a range-based loop is a pattern you will use constantly:
#include <iostream>
#include <vector>
int main() {
std::vector<double> scores = {88.0, 72.5, 95.0, 61.0, 83.5};
double total = 0.0;
for (double s : scores) {
total += s;
}
double average = total / static_cast<double>(scores.size());
std::cout << "Average: " << average << "\n";
return 0;
}Passing a vector to a function
Pass by const reference to avoid copying the entire vector on every call:
#include <iostream>
#include <vector>
void printAll(const std::vector<int>& items) {
for (int item : items) {
std::cout << item << " ";
}
std::cout << "\n";
}
int main() {
std::vector<int> nums = {3, 1, 4, 1, 5, 9};
printAll(nums);
return 0;
}What Can Go Wrong
Out-of-bounds access
Accessing an index that does not exist is undefined behavior β your program may crash, produce garbage output, or silently corrupt memory. The compiler will not warn you.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {10, 20, 30};
// BAD: index 3 does not exist; valid indices are 0, 1, 2
std::cout << v[3] << "\n"; // undefined behavior β do not do this
// GOOD: .at() checks the index and throws std::out_of_range if invalid
std::cout << v.at(3) << "\n"; // throws, which is safer than silent corruption
return 0;
}Use .at(i) while learning and debugging. Once you have verified your index logic is correct, v[i] is fine for performance-sensitive code.
Forgetting that size() is unsigned
vector::size() returns an unsigned integer (std::size_t). Comparing it against a signed int can cause subtle bugs. Prefer either std::size_t as your loop variable or the range-based for loop:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3};
// GOOD: use std::size_t to match the type of .size()
for (std::size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << "\n";
}
return 0;
}When you only need values and not the index, the range-based for loop sidesteps this entirely.
Modifying a vector while iterating over it
Adding or removing elements inside a range-based for loop invalidates the loop's internal state, causing undefined behavior. Collect changes separately and apply them after the loop:
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3};
// BAD: push_back inside a range-for is undefined behavior
// for (int x : v) { v.push_back(x * 2); }
// GOOD: gather new values first, then append
std::vector<int> extras;
for (int x : v) {
extras.push_back(x * 2);
}
for (int x : extras) {
v.push_back(x);
}
return 0;
}Quick Reference
| Feature | Raw Array | std::vector |
|---|---|---|
| Size fixed at compile time | Yes | No |
| Resize at runtime | No | Yes β push_back, pop_back |
| Query number of elements | Manual tracking required | .size() |
| Safe bounds-checked access | None built-in | .at(i) throws on bad index |
| Header required | None | <vector> |
| Pass to function | Decays to pointer, size lost | Pass by reference, size preserved |
| Initialize with values | int a[3] = {1, 2, 3}; | vector<int> v = {1, 2, 3}; |
Rule of thumb: default to std::vector. Use a raw array only when you need a compile-time-fixed buffer or must pass a plain pointer to a C API. Use std::array<T, N> (from <array>) as a safer drop-in for fixed-size arrays β it knows its own size and doesn't decay to a pointer.
#include <array>
std::array<int, 5> scores {91, 78, 85, 62, 99};
std::cout << scores.size() << "\n"; // 5 β unlike raw arrays, size is always availableWhat's Next
- Strings and Text β
std::stringworks much likestd::vectorand is the standard way to handle text in C++. - Functions β package your loops and computations into reusable, named blocks.
- References and Pointers β understand exactly what happens when you pass a vector to a function by reference.
- Range Algorithms β the
<algorithm>header providessort,find,count, and dozens more operations that work directly on vectors.
Capacity vs Size
std::vector tracks two distinct numbers: how many elements it currently holds (size) and how much memory it has already reserved (capacity). This distinction matters because growing the vector beyond its current capacity triggers a heap allocation and copies all existing elements to a new, larger buffer β an expensive operation if it happens repeatedly.
When you know ahead of time how many elements you will need, call reserve before any push_back calls. The vector allocates enough memory in one shot and avoids reallocation entirely.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.reserve(1000); // allocate memory for 1000 ints β no elements yet
std::cout << "size: " << v.size() << "\n"; // 0
std::cout << "capacity: " << v.capacity() << "\n"; // 1000 (at least)
for (int i = 0; i < 1000; ++i) {
v.push_back(i); // never triggers a reallocation
}
std::cout << "size: " << v.size() << "\n"; // 1000
std::cout << "capacity: " << v.capacity() << "\n"; // 1000
}After removing many elements you may want to release unused memory. shrink_to_fit is a hint to the implementation to reduce capacity to match size β most implementations honor it.
#include <vector>
int main() {
std::vector<int> v(10000, 42); // 10 000 elements
v.clear(); // size β 0, capacity unchanged
std::cout << v.capacity() << "\n"; // still large
v.shrink_to_fit(); // releases the memory
std::cout << v.capacity() << "\n"; // 0 or very small
}The "shrink-to-fit swap" idiom
std::vector<int>().swap(v)achieves the same effect on older compilers that ignoredshrink_to_fit.
std::array vs C Arrays vs std::vector
When the number of elements is fixed at compile time, std::array<T, N> gives you the safety of std::vector with the storage cost of a raw C array β everything lives on the stack. Choosing between the three types comes down to whether size is known at compile time and whether it can change at runtime.
#include <array>
#include <vector>
#include <iostream>
int main() {
// C array β fixed size, no bounds checking, size lost when passed to functions
int c_arr[4] = {10, 20, 30, 40};
// std::array β fixed size, knows its own size, stack-allocated
std::array<int, 4> std_arr = {10, 20, 30, 40};
std::cout << std_arr.size() << "\n"; // always 4
std::cout << std_arr.at(1) << "\n"; // 20, throws on bad index
// std::vector β runtime size, heap-allocated, can grow
std::vector<int> vec = {10, 20, 30, 40};
vec.push_back(50);
std::cout << vec.size() << "\n"; // 5
}int arr[N] | std::array<int, N> | std::vector<int> | |
|---|---|---|---|
| Size known at compile time | Required | Required | Not required |
| Resizable at runtime | No | No | Yes |
| Storage location | Stack | Stack | Heap |
| Bounds-checked access | None built-in | .at(i) | .at(i) |
| Knows its own size | No (decays to pointer) | .size() | .size() |
| Works with range-based for | Yes | Yes | Yes |
| Header | None | <array> | <vector> |
The rule: use std::array when size is a compile-time constant and you want stack allocation without the footguns of raw C arrays. Use std::vector for everything else.
2D Vectors and Flat 1D Alternatives
A vector-of-vectors is the most readable way to represent a grid or matrix in C++, but a single flat vector accessed with index arithmetic is often faster due to better cache locality. Both approaches are correct; the right choice depends on whether you need to resize rows independently or want maximum iteration speed.
The vector-of-vectors approach treats each row as its own std::vector:
#include <iostream>
#include <vector>
int main() {
// 3 rows, 4 columns, all zeros
std::vector<std::vector<int>> grid(3, std::vector<int>(4, 0));
grid[1][2] = 42; // row 1, column 2
for (const auto& row : grid) {
for (int val : row) {
std::cout << val << " ";
}
std::cout << "\n";
}
}Output:
0 0 0 0
0 0 42 0
0 0 0 0Each row is a separate heap allocation, so iterating across rows jumps around in memory. A flat 1D vector with manual index arithmetic keeps everything contiguous:
#include <iostream>
#include <vector>
int main() {
const int rows = 3, cols = 4;
std::vector<int> grid(rows * cols, 0); // single allocation
// access element at (r, c): index = r * cols + c
grid[1 * cols + 2] = 42;
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
std::cout << grid[r * cols + c] << " ";
}
std::cout << "\n";
}
}The flat layout is typically 2β5Γ faster to iterate because all data is in one contiguous block and the CPU cache prefetcher can stream through it efficiently. The trade-off is that index math is manual and rows cannot differ in length.
Iterator Invalidation
Iterators β and pointers into a vector β become dangling the moment the vector reallocates its internal buffer. Any operation that may increase the vector's size beyond its current capacity (such as push_back, insert, or emplace_back) can trigger a reallocation and leave all existing iterators pointing at freed memory.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3};
auto it = v.begin(); // iterator to the first element
v.push_back(4); // may reallocate β 'it' is now dangling!
// BAD: using 'it' after potential reallocation is undefined behavior
// std::cout << *it << "\n";
// GOOD: re-obtain the iterator after modification
it = v.begin();
std::cout << *it << "\n"; // safe: 1
}Operations that always invalidate all iterators: push_back / emplace_back when size() == capacity(), insert, erase, resize, reserve, clear.
The safe pattern is to not hold iterators across any mutating calls. If you must insert at multiple positions, complete all insertions first and then iterate:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 3, 5, 7};
// insert in the middle β everything after the insertion point is invalidated
auto pos = v.begin() + 2;
v.insert(pos, 99); // pos is now invalid; do not use it again
for (int x : v) {
std::cout << x << " "; // 1 3 99 5 7
}
}Pre-allocating with reserve eliminates reallocation during push_back calls, which prevents iterator invalidation for that specific case. Other mutating operations can still invalidate iterators regardless of capacity.
std::span β a Lightweight View
std::span (C++20) is a non-owning view over any contiguous sequence of elements β a raw array, std::array, or std::vector β without copying a single byte. It is the modern replacement for the classic (T* data, size_t len) parameter pair you see everywhere in C APIs.
#include <iostream>
#include <span>
#include <vector>
#include <array>
// accepts any contiguous container β no template needed, no copy made
void printAll(std::span<const int> items) {
for (int x : items) {
std::cout << x << " ";
}
std::cout << "\n";
}
int main() {
std::vector<int> v = {1, 2, 3, 4};
std::array<int, 3> a = {10, 20, 30};
int c[2] = {100, 200};
printAll(v); // works
printAll(a); // works
printAll(c); // works β all contiguous
}A span carries a pointer and a length; it is two words wide and trivially copyable. You can also take a sub-span to slice a range without any allocation:
#include <iostream>
#include <span>
#include <vector>
int main() {
std::vector<int> v = {10, 20, 30, 40, 50};
std::span<int> all = v;
std::span<int> mid = all.subspan(1, 3); // elements at index 1, 2, 3
for (int x : mid) {
std::cout << x << " "; // 20 30 40
}
}Use std::span<const T> when the function should only read, and std::span<T> when it may write. If you are still on C++17, pass (const T* data, std::size_t len) or a const std::vector<T>&.
Removing Elements by Value
The erase-remove idiom is the standard way to delete all elements matching a condition from a std::vector in a single efficient pass. Without it, naive loops that erase inside a loop are slow (O(nΒ²)) or produce undefined behavior.
std::remove from <algorithm> shuffles matching elements to the end and returns an iterator to the new logical end. You then call .erase to chop them off:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 4, 2, 4, 3, 4};
// move all 4s to the end, get iterator to new logical end
auto new_end = std::remove(v.begin(), v.end(), 4);
// erase the "garbage" tail
v.erase(new_end, v.end());
for (int x : v) {
std::cout << x << " "; // 1 2 3
}
}C++20 introduced the cleaner free functions std::erase and std::erase_if that do both steps at once:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 4, 2, 4, 3, 4};
std::erase(v, 4); // remove all 4s (C++20)
// or by predicate:
// std::erase_if(v, [](int x){ return x % 2 == 0; }); // remove evens
for (int x : v) {
std::cout << x << " "; // 1 2 3
}
}To split a vector into elements that satisfy a condition and those that do not β without removing either group β use std::partition:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
// partition: even numbers first, odd numbers second
auto mid = std::partition(v.begin(), v.end(),
[](int x){ return x % 2 == 0; });
// elements before mid satisfy the predicate
for (auto it = v.begin(); it != mid; ++it) {
std::cout << *it << " "; // 2 4 6 (order not guaranteed)
}
std::cout << "| ";
for (auto it = mid; it != v.end(); ++it) {
std::cout << *it << " "; // 1 3 5
}
}