← Back to blog

C++ Array vs Vector: When and Why to Use Each

C++ offers several “array-like” containers, but the most common day-to-day choice is between fixed-size inline storage (built-in arrays / std::array) and a dynamically sized owning container (std::vector). They can look similar—both store elements contiguously—but their tradeoffs are very different.

Quick rule of thumb

  • Use std::vector<T> when the size is dynamic/unknown or you need push/pop.
  • Use std::array<T, N> when the size is fixed at compile time and you want inline storage.
  • Use a built-in array (T[N]) mostly for C interop or very low-level code (and consider wrapping it in std::span).

A concrete decision example:

  • “I’m storing exactly 3 floats in every vertex” → std::array<float, 3>
  • “I’m reading lines from a file and don’t know how many” → std::vector<std::string>

(Headers are omitted in many snippets for brevity unless they’re the point.)

The three “array” options you’ll actually see

Before comparing, it helps to separate these clearly:

  • Built-in array (T[N]) (sometimes called “C array”): int a[10];

    • Size is part of the type and is fixed.
    • Often lives in automatic storage when declared locally, but it can also have static storage (static int a[10];) or be a member subobject inside another object.
    • Decays to pointer in many contexts, losing size information.
    • Note: Standard C++ does not have variable-length arrays (VLAs). Some compilers accept int a[n]; as an extension, but it’s not portable C++.
  • std::array<T, N>: std::array<int, 10> a;

    • Fixed size known at compile time.
    • Inline storage: the elements are stored inside the std::array object, wherever that object lives (stack, static storage, inside another object, heap, etc.).
    • Doesn’t decay to pointer; has .size(), iterators, and works well with STL algorithms.
  • std::vector<T>: std::vector<int> v;

    • Size decided at runtime; can grow/shrink.
    • Owns a dynamically allocated buffer when it needs elements (commonly when size > 0). An empty vector typically holds no allocation, and moved-from vectors may also be empty.
    • Rich API, manages lifetime, size, and capacity.

When people say “array vs vector”, in modern C++ they usually mean std::array vs std::vector.

Key differences

1) Size: fixed vs dynamic

  • Built-in arrays and std::array<T, N> have fixed size.

    • Great when the size is a known constant.
    • No growth and no reallocation.
  • std::vector<T> is dynamic.

    • Use when you don’t know the size at compile time, or you need to add/remove elements.

Example:

#include <array>
#include <vector>

std::array<float, 3> rgb = {1.0f, 0.5f, 0.0f}; // fixed: always 3

std::vector<float> samples;
samples.push_back(0.1f);
samples.push_back(0.2f); // grows as needed

2) Ownership and lifetime

  • A built-in array declared in a function has automatic storage duration (ends at scope exit):
void f() {
    int a[100];
    // a is destroyed at end of scope
}
  • But a built-in array can also have static storage duration:
void g() {
    static int a[100];
    // exists for the program lifetime
}
  • std::array stores elements inline, so its lifetime is simply the lifetime of the std::array object.

  • std::vector owns its buffer and releases it when the vector is destroyed.

#include <vector>

void h() {
    std::vector<int> v(100); // typically allocates storage for 100 ints
} // v destroys elements and releases its buffer

This makes std::vector a safe default for returning variable-length sequences from functions.

3) Safety and ergonomics (especially size information)

Built-in arrays are easy to misuse because they decay to pointers:

void takes_ptr(int* p) {
    // no idea how many elements p points to
}

int a[10];
takes_ptr(a); // size information lost

std::array and std::vector keep size information:

#include <array>
#include <vector>

void takes_vector(const std::vector<int>& v) {
    (void)v.size();
}

void takes_array(const std::array<int, 10>& a) {
    (void)a.size();
}

They also integrate cleanly with range-based for loops, iterators, and algorithms.

4) Initialization differences (a common real-world pitfall)

Initialization rules differ in ways that cause bugs:

int a[10];    // uninitialized (for automatic storage); values are indeterminate
int b[10]{};  // zero-initialized

With std::array, you can (and usually should) value-initialize similarly:

#include <array>

std::array<int, 10> x;   // elements are uninitialized (same pitfall as raw array)
std::array<int, 10> y{}; // elements are value-initialized (ints become 0)

With std::vector, construction typically initializes elements:

#include <vector>

std::vector<int> v(10);     // 10 value-initialized ints (0)
std::vector<int> w;         // empty
w.resize(10);               // now size is 10; new elements value-initialized

5) Performance: what actually matters

All three options store elements contiguously, so iteration/cache locality is often similar for the element buffer itself. The bigger differentiators are:

  • Allocation and indirection (std::vector):

    • The vector object points to a separate buffer (extra pointer chase compared to inline storage).
    • Creating/growing a vector may allocate/reallocate.
  • Reallocation and moves/copies (std::vector):

    • When a vector grows beyond capacity, it reallocates and moves/copies elements.
    • If T is expensive to move/copy, growth can be costly.
  • Predictability (built-in arrays / std::array):

    • No reallocations because size never changes.

If you know the final size up front, you can keep std::vector fast by reserving:

#include <vector>

std::vector<int> v;
v.reserve(1'000'000); // reserve changes capacity, not size
for (int i = 0; i < 1'000'000; ++i) {
    v.push_back(i);
}

A frequent confusion: reserve() vs resize()

  • reserve(n): ensures capacity >= n; does not create elements; size() unchanged.
  • resize(n): changes size() to n; constructs/destroys elements as needed.

6) Iterator/reference invalidation

This is a key behavioral difference in real code:

  • Built-in arrays and std::array never reallocate, so references/iterators to elements remain valid as long as the array object itself is alive.

  • std::vector can invalidate references, pointers, and iterators:

    • Reallocation (commonly triggered by growth) invalidates all pointers/references/iterators to elements.
    • Even without reallocation, operations like erase invalidate iterators/references at and after the point of erasure.

If you store pointers/iterators into a vector, you must account for possible invalidation.

7) Memory layout and “overhead”

  • std::array<T, N> is essentially N elements stored inline.

    • sizeof(std::array<T, N>) is roughly N * sizeof(T) (plus any padding for alignment).
  • std::vector<T> is a small control object plus a separate element buffer.

    • Many implementations store something like three machine words worth of state (pointer + size + capacity), but the exact layout is not guaranteed by the standard.

This matters in tight data structures:

  • If you have a struct with a small fixed set of values, std::array keeps everything inline.
  • If you have many objects each containing a vector, you may create many separate allocations (allocator overhead/fragmentation). If allocation behavior matters, consider allocator-aware approaches such as std::pmr::vector.

When to use a built-in array (T[N])

Use built-in arrays mainly when:

  • You need interoperability with C APIs that require T* plus a length.
  • You’re writing very low-level code where you intentionally want minimal abstraction.

Even then, prefer to preserve size information in C++ code by using std::span (C++20) or std::size (C++17) when calling C.

Example (C interop, avoiding magic numbers):

#include <cstddef>
#include <iterator> // std::size

extern "C" void c_api_process(const float* data, std::size_t len);

void call_c() {
    float data[256]{};
    c_api_process(data, std::size(data));
}

Also note that std::array interops cleanly too:

#include <array>
#include <cstddef>

extern "C" void c_api_process(const float* data, std::size_t len);

void call_c2() {
    std::array<float, 256> data{};
    c_api_process(data.data(), data.size());
}

When to use std::array

Prefer std::array when:

  • The size is fixed and known at compile time.
  • You want inline storage (often beneficial for locality and avoiding allocations).
  • You want a container that behaves well with STL algorithms.

Common examples:

  • 2D/3D vectors, matrices with fixed dimensions
  • Fixed protocol headers
  • Lookup tables with compile-time size
#include <array>

struct Vertex {
    std::array<float, 3> pos{};
    std::array<float, 3> normal{};
};

When to use std::vector

Prefer std::vector when:

  • The number of elements is not known until runtime.
  • You need to add/remove elements.
  • You want a safe default owning “dynamic array”.

Examples:

  • Reading a file of unknown size
  • Collecting results from a computation
  • Storing entities in a game loop
#include <string>
#include <vector>
#include <istream>

std::vector<std::string> read_lines(std::istream& in) {
    std::vector<std::string> lines;
    for (std::string s; std::getline(in, s); ) {
        lines.push_back(std::move(s));
    }
    return lines;
}

As a modern aside: std::string is essentially a specialized “vector-like” owning container for characters (with additional string-specific APIs).

API design: what should your function take?

A common mistake is over-committing to std::vector in function parameters. If a function only needs a view of contiguous elements, prefer std::span (C++20):

#include <array>
#include <span>
#include <vector>

double sum(std::span<const double> xs) {
    double s = 0;
    for (double x : xs) s += x;
    return s;
}

void demo() {
    double raw[3]{1, 2, 3};
    std::array<double, 3> a{1, 2, 3};
    std::vector<double> v{4, 5, 6};

    sum(raw); // ok
    sum(a);   // ok
    sum(v);   // ok
}

Two important cautions:

  • std::span is non-owning. Don’t return a span to a local array/vector:
#include <span>
#include <vector>

std::span<int> bad() {
    std::vector<int> v{1,2,3};
    return v; // dangling span after return
}
  • std::span requires contiguous storage, so it works with T[N], std::array, and std::vector, but not with non-contiguous containers like std::list.

If you must support pre-C++20, consider iterator pairs or gsl::span.

Common pitfalls and how to avoid them

1) Returning pointers to local arrays

int* bad() {
    int a[10];
    return a; // dangling pointer
}

Return std::array by value for fixed size, or std::vector for dynamic size.

2) Using std::vector for tiny fixed-size data

If you always have exactly 3 elements, std::vector adds indirection and typically an allocation for no benefit. Use std::array<T, 3>.

3) Forgetting to reserve (and confusing reserve vs resize)

If you know (even approximately) how many elements you’ll push, reserve() can prevent repeated reallocations.

Remember: reserve() adjusts capacity; resize() adjusts size and constructs elements.

4) Assuming std::vector is “slow”

For many workloads, std::vector is among the fastest general-purpose containers because of contiguous storage and excellent cache behavior. The main costs are allocation, possible reallocation, and the extra level of indirection—often manageable with good usage patterns.

Recap checklist

Choose std::array<T, N> when:

  • Size is fixed at compile time
  • You want inline storage (wherever the object lives)
  • You want STL-friendly behavior without dynamic allocation

Choose std::vector<T> when:

  • Size is dynamic or unknown
  • You need to grow/shrink
  • You want an owning container for variable-length sequences

Use built-in arrays (T[N]) mostly when:

  • You’re interfacing with C
  • You’re doing specialized low-level work and accept decay-to-pointer and initialization pitfalls

For function parameters, consider std::span to accept both arrays and vectors without copying—while respecting lifetime requirements.