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
| Aspect | C array | std::array | std::vector |
|---|---|---|---|
| Element storage | Where declared (stack, static, inside an object) | Where declared | Heap buffer owned by the vector |
| Size | Compile-time constant | Compile-time constant (template parameter) | Runtime, can grow |
sizeof of the object | N × sizeof(T) | N × sizeof(T) | Size of three pointers, independent of N |
| Copy / assign / return | Not allowed (decays) | Element-wise copy | Deep copy (allocates) |
| Bounds-checked access | No | at() | at() |
| Passed to a function | Decays to T*, size lost | By value or reference, size kept | Usually 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
vectorcalls the allocator, and destroying it frees memory. Astd::arraylocal 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 (callclear(), which keeps the capacity) or switching tostd::arrayremoves it. - Growth.
push_backbeyond capacity reallocates and moves every element.reserve()avoids that when you know the final size (see vector reserve vs resize). - Copying. Copying a
vectorallocates and copies all elements. Copying a largestd::arrayalso copies all elements, with no allocation. Passing either by value when you meantconst&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
| Situation | Prefer | Why |
|---|---|---|
| Size unknown or changes | std::vector | Only option that grows |
| Size fixed and small, created often | std::array | No allocation, inline storage |
| Fixed-size member of a struct (coordinates, RGB, matrix 4×4) | std::array | Value semantics, copyable, no indirection |
| Large buffer, even if fixed | std::vector | Avoids stack overflow |
| Passing to a C API | .data() + .size() from either | Keep ownership in a C++ container |
| Function parameter accepting any contiguous range | std::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.