Skip to content
C++
Language
since C++11
Beginner

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:

cpp
int score0 = 91;
int score1 = 78;
int score2 = 85;
// ...seven more variables

This 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.

cpp
#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:

cpp
#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:

cpp
#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.

cpp
#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:

cpp
#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):

cpp
#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():

cpp
#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:

cpp
#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:

cpp
#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:

cpp
#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.

cpp
#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:

cpp
#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:

cpp
#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

FeatureRaw Arraystd::vector
Size fixed at compile timeYesNo
Resize at runtimeNoYes β€” push_back, pop_back
Query number of elementsManual tracking required.size()
Safe bounds-checked accessNone built-in.at(i) throws on bad index
Header requiredNone<vector>
Pass to functionDecays to pointer, size lostPass by reference, size preserved
Initialize with valuesint 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.

cpp
#include <array>

std::array<int, 5> scores {91, 78, 85, 62, 99};
std::cout << scores.size() << "\n";   // 5 β€” unlike raw arrays, size is always available

What's Next

  • Strings and Text β€” std::string works much like std::vector and 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 provides sort, 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.

cpp
#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.

cpp
#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 ignored shrink_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.

cpp
#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 timeRequiredRequiredNot required
Resizable at runtimeNoNoYes
Storage locationStackStackHeap
Bounds-checked accessNone built-in.at(i).at(i)
Knows its own sizeNo (decays to pointer).size().size()
Works with range-based forYesYesYes
HeaderNone<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:

cpp
#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:

cpp
0 0 0 0
0 0 42 0
0 0 0 0

Each 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:

cpp
#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.

cpp
#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:

cpp
#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.

cpp
#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:

cpp
#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:

cpp
#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:

cpp
#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:

cpp
#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
    }
}