C++ Array vs vector: Performance, Safety, and When to Use

Key takeaways

C arrays, std::array, and std::vector compared: where the memory lives, pointer decay, bounds checks, copy semantics, and where the real performance cost is.

Introduction

C++ gives you three ways to hold a sequence of elements next to each other in memory:

int arr1[5] = {1, 2, 3, 4, 5};                 // C-style array
std::array<int, 5> arr2 = {1, 2, 3, 4, 5};     // fixed size, C++11
std::vector<int> vec = {1, 2, 3, 4, 5};        // dynamic size, heap storage

All three store elements contiguously, so indexing, iteration and cache behavior are the same once the data exists. They differ in who owns the memory, whether the size can change, and how they behave when you pass, copy, or return them. Those differences, not raw speed, should decide which one you pick.

For the algorithmic side (arrays vs linked lists, insertion cost, cache locality), see Arrays and Lists.

The three side by side

AspectC arraystd::arraystd::vector
Element storageWhere declared (stack, static, inside an object)Where declaredHeap buffer owned by the vector
SizeCompile-time constantCompile-time constant (template parameter)Runtime, can grow
sizeof of the objectN × sizeof(T)N × sizeof(T)Size of three pointers, independent of N
Copy / assign / returnNot allowed (decays)Element-wise copyDeep copy (allocates)
Bounds-checked accessNoat()at()
Passed to a functionDecays to T*, size lostBy value or reference, size keptUsually const&, size kept

A quick check with GCC 10 on 64-bit Linux:

int a[5];                      // sizeof(a) == 20
std::array<int, 5> b;          // sizeof(b) == 20
std::vector<int> v(5);         // sizeof(v) == 24 (begin, end, capacity pointers)

std::array is a struct wrapping a C array, with no extra members. That is why it costs nothing extra. std::vector is a small handle, and the elements live elsewhere. That extra layer causes both its flexibility (it can grow) and its cost (it has to allocate).

“Stack vs heap” is a simplification. A std::array member of a heap-allocated object lives on the heap, and a static C array lives in the data segment. The accurate statement: std::array and C arrays store elements inline, wherever the enclosing object is. vector always stores them in a separate allocation.

Pointer decay: the C array’s main problem

void foo(int arr[]) {
    std::cout << sizeof(arr) << "\n";   // prints 8, not 20
}

The parameter int arr[] (or even int arr[5]) is rewritten by the compiler to int* arr. The length is gone. GCC 10 catches the obvious case:

warning: 'sizeof' on array function parameter 'arr' will return size of 'int*' [-Wsizeof-array-argument]

It does not catch the general case, where a function takes int* and a separate n and someone passes the wrong n. That mistake causes most buffer overflows in C-style code. The failure is often silent: writing arr[5] into a 5-element local array overwrites whatever the compiler placed next to it, and the program may run normally for months until the stack layout changes.

C arrays also cannot be copied or assigned as a whole:

int a[5] = {};
int d[5] = a;   // error: array must be initialized with a brace-enclosed initializer

std::array has none of these issues. std::array<int, 5> c = b; copies, b == c compares element-wise, and a function can return one by value. When one function needs to accept any contiguous sequence (C array, std::array, or vector) without templates, C++20’s std::span passes pointer and length together.

Bounds checking: at() vs operator[]

Both containers offer two access paths:

std::vector<int> v = {1, 2, 3, 4, 5};
v[10];       // undefined behavior, no check
v.at(10);    // throws std::out_of_range

With libstdc++ the exception messages are specific:

vector::_M_range_check: __n (which is 10) >= this->size() (which is 5)
array::at: __n (which is 10) >= _Nm (which is 5)

at() adds a compare and a branch. In a tight loop where the index comes from the loop bounds, that check is redundant, and operator[] is fine. at() is useful when the index comes from outside (a file, a network message, user input). There an exception is much better than silent memory corruption. For catching operator[] mistakes during development without changing code, build with -D_GLIBCXX_ASSERTIONS (libstdc++) or run under AddressSanitizer. Both turn out-of-range indexing into an immediate, reported failure.

Where the performance difference really is

Element access is not where these containers differ. v[i] on a vector and a[i] on an array both compile to a single load from base + i * sizeof(T) at -O2. The vector’s base pointer lives in the vector object and may need one extra load, which the optimizer usually hoists out of the loop.

The real differences are elsewhere:

  • Construction and destruction. Creating a vector calls the allocator, and destroying it frees memory. A std::array local is just stack space. If a function that runs millions of times builds a small temporary vector, the allocation can dominate the function’s cost. Reusing one vector (call clear(), which keeps the capacity) or switching to std::array removes it.
  • Growth. push_back beyond capacity reallocates and moves every element. reserve() avoids that when you know the final size (see vector reserve vs resize).
  • Copying. Copying a vector allocates and copies all elements. Copying a large std::array also copies all elements, with no allocation. Passing either by value when you meant const& is a common silent cost.

Measure your own case with a profiler before switching containers for speed. In most code the choice makes no measurable difference, and the safety properties matter more.

The stack-size trap with std::array

void process() {
    std::array<double, 1'000'000> samples;  // 8 MB on the stack
    // ...
}

This compiles cleanly and then crashes with a segmentation fault (or 0xC00000FD stack overflow on Windows) the moment the function is entered. The main thread’s stack is typically 8 MB on Linux and 1 MB on Windows, and secondary threads often get less. I have seen this pattern appear when someone replaces a vector with std::array “for performance”: it works in a unit test with a small N, and then N grows. For large or unknown sizes, use vector, or wrap the array in std::unique_ptr<std::array<...>> if the size really is fixed. More on this in stack overflow errors.

Another failure I often see with vectors: holding a pointer or reference into one across a push_back. When the push triggers a reallocation, data() moves and the old pointer dangles. Neither array type has this problem, because its storage never moves. See iterator invalidation.

Choosing

SituationPreferWhy
Size unknown or changesstd::vectorOnly option that grows
Size fixed and small, created oftenstd::arrayNo allocation, inline storage
Fixed-size member of a struct (coordinates, RGB, matrix 4×4)std::arrayValue semantics, copyable, no indirection
Large buffer, even if fixedstd::vectorAvoids stack overflow
Passing to a C API.data() + .size() from eitherKeep ownership in a C++ container
Function parameter accepting any contiguous rangestd::span (C++20) or const std::vector<T>&Length travels with data
struct Position {
    std::array<float, 3> coords;   // copyable, comparable, 12 bytes inline
};

std::array<char, 1024> buffer;
readData(buffer.data(), buffer.size());   // C API boundary

std::vector<int> numbers;
numbers.reserve(n);
for (int i = 0; i < n; ++i) numbers.push_back(i);

Summary

Default to std::vector. Use std::array when the size is a small compile-time constant, especially for struct members and short-lived locals. Keep raw C arrays at C interfaces, and immediately wrap what they give you. Speed is rarely a reason to choose between them. Ownership, lifetime, and the risk of losing the length are.