C++ Part I · Module 19 · Standard Library Containers
std::array and std::vector
These are the two containers that store their elements in one unbroken block of memory, and that single shared property is why they are the right starting point and the right default. Contiguity is worth more than it sounds. It makes reaching the nth element arithmetic rather than a search, it makes the hardware's prefetcher able to predict what you will touch next — the effect Chapter 19.8 measures as the dominant term in real performance — and it means the elements can be handed to any C interface in the world as a pointer and a length, which no other standard container can do. The two differ in exactly one respect, and everything else follows from it: std::array's size is fixed at compile time and is part of its type, while std::vector's size is a run-time property and its storage comes from the heap. That difference decides where the elements live, whether the container can grow, how much the container object itself costs, and whether a function can accept sequences of different lengths through the same parameter. This chapter measures all four consequences, and then makes the argument the frozen curriculum asks for: that std::vector is the correct default for a sequence in almost every case, not because it is fastest at everything, but because the cases where something else wins are narrow, nameable, and rarer than habit suggests.
Foundation18 min readC++Part IFundamentalsContainers
Part I · Module 19 · Chapter 19.2 · std::array and std::vector
1. One Property, Two Containers
Both of these containers guarantee that their elements occupy a single contiguous block of memory, with element n at base + n * sizeof(T) and nothing in between. The standard says so in as many words, and §4 measures it.
That guarantee is the reason they come first in this module. It gives three things at once. Indexing is address arithmetic, so v[40000] costs what v[0] costs. Traversal moves linearly through memory, which is the access pattern the hardware is built for. And &v[0] is a pointer to the first of n consecutive T objects, which is precisely the calling convention that every C library on the planet expects.
They differ in where that block comes from. std::array<T, N> is the block — the elements are members of the object, so an array declared as a local variable puts its elements on the stack and never calls the allocator. std::vector<T> holds a pointer to a block it obtained from the allocator, which is why it can grow and why it costs an allocation.
Everything else in this chapter is a consequence of that one difference.
2. What the Container Object Costs
std::printf("sizeof(int[8]) = %2zu\n", sizeof(int[8]));
std::printf("sizeof(std::array<int,8>) = %2zu\n", sizeof(std::array<int,8>));
std::printf("sizeof(std::vector<int>) = %2zu\n", sizeof(std::vector<int>));
std::printf("sizeof(std::array<int,1024>)= %zu\n", sizeof(std::array<int,1024>));sizeof(int[8]) = 32
sizeof(std::array<int,8>) = 32 (identical: no bookkeeping at all)
sizeof(std::vector<int>) = 24 (3 pointers, whatever the element count)
sizeof(std::array<int,1024>)= 4096 (the elements ARE the object)
is_trivially_copyable<array<int,8>> = 1Four facts in that output, and all four get used later.
std::array adds exactly nothing. Thirty-two bytes for eight ints is eight ints. There is no stored size, because the size is in the type; there is no pointer, because there is nothing to point at. It is a struct wrapping a built-in array, and it compiles to the same thing. Chapter 18.5 §4 measured the same property for a non-type-parameterised class in general — the storage follows the parameter exactly — and std::array is that argument's canonical instance in the standard library.
std::vector costs 24 bytes regardless. Three pointers: the start of the block, the end of the used part, and the end of the allocated part. A vector of ten million elements has the same 24-byte object as an empty one; the ten million elements are somewhere else.
std::array's size scales the object. A 1024-element array is a 4096-byte object. This matters because that object has to live somewhere, and §3 is about where.
std::array is trivially copyable. Copying one is a memcpy, with no constructor calls. That follows from being a plain aggregate and it is why an array can be copied with memcpy — subject to the layout caveats Chapter 9.8 §9 raises about writing a structure out bytewise.
3. Where the Elements Live
std::array<int,8> a{}; std::vector<int> v(8);
int local = 0;
std::printf("a stack variable is at %p\n", (void*)&local);
std::printf("std::array elements are at %p\n", (void*)a.data());
std::printf("std::vector elements are at %p\n", (void*)v.data());a stack variable is at 0x16fa0e6d0
std::array elements are at 0x16fa0e6b0 (32 bytes from that stack slot)
std::vector elements are at 0x100a11a00 (a different region entirely: the heap)The array's elements are 32 bytes from an ordinary local variable — they are on the stack, in the same frame. The vector's elements are in a completely different part of the address space.
The allocation count makes the same point without pointer arithmetic:
std::array<int,1000> allocs=0 bytes=0
vector<int>(1000) allocs=1 bytes=4000Both figures come from the counting allocator this module uses throughout, which records every call the container makes.
Two practical consequences follow. First, a large std::array as a local variable consumes stack, and stack is a small, fixed resource — a few megabytes typically, with no graceful failure when you exhaust it. std::array<double, 1'000'000> is an eight-megabyte local variable and will usually crash on entry to the function, which is a good reason for the size rule in §13. Second, std::array works where there is no heap at all: before main runs, inside an interrupt handler, in a freestanding environment, or anywhere an allocation is forbidden. That is a real niche and it is std::array's strongest claim.
4. Contiguity and C Interoperability
static int sumC(const int* p, std::size_t n) // an ordinary C function
{ int s = 0; for (std::size_t i = 0; i < n; ++i) s += p[i]; return s; }
std::vector<int> v{1,2,3,4,5};
std::array<int,5> a{1,2,3,4,5};
std::printf("&v[1] - &v[0] = %ld element\n", (long)(&v[1] - &v[0]));
std::printf("sumC(v.data(), v.size()) = %d\n", sumC(v.data(), v.size()));
std::printf("sumC(a.data(), a.size()) = %d\n", sumC(a.data(), a.size()));
std::printf("memcmp(v.data(), a.data(), 20) = %d\n", std::memcmp(v.data(), a.data(), 20));&v[1] - &v[0] = 1 element (contiguous, guaranteed)
sumC(v.data(), v.size()) = 15
sumC(a.data(), a.size()) = 15
memcmp(v.data(), a.data(), 20) = 0 (byte-identical payloads)The memcmp returning zero is the clean statement of the property: twenty bytes starting at v.data() are bit-for-bit the same as twenty bytes starting at a.data(). Both containers hold five ints with nothing interleaved, which is why a C function written decades ago accepts either.
data() is the member that makes this official. It returns a pointer to the first element, and it is valid to treat it as the base of an n-element array. Note that data() exists on exactly the contiguous containers — array, vector, and std::string — and on no others. Chapter 19.4 §4 measures why std::deque cannot offer it despite having operator[].
One caveat worth stating because it is a real bug: v.data() on an empty vector may be nullptr, and &v[0] on an empty vector is undefined behaviour. Pass data() and size() together and let the callee loop zero times.
5. Growth Is the Whole Difference
std::array has no push_back, no insert, no resize, no clear. Its size is a template argument, fixed when the code was compiled, and there is no mechanism by which it could change. std::vector has all of them.
The cost of that capability is the subject of Chapter 19.3, but the headline belongs here because it is the main argument against reaching for vector thoughtlessly:
1000 x push_back (no reserve) allocs=11 bytes=8188
1000 x push_back (reserved) allocs= 1 bytes=4000Growing to a thousand elements one at a time performed eleven allocations and asked the allocator for 8,188 bytes to store 4,000 bytes of data. Telling the vector the final size first made it one allocation of exactly 4,000 bytes. Nothing about the resulting container differs; the elements are the same and the final capacity is the same. The difference is entirely in the path taken to get there.
So "std::vector is the default" does not mean "growth is free." It means the capability is there when you need it and costs almost nothing when you do not use it, provided you tell it the size you know.
6. The Size Is Part of the Type, or It Is Not
is_same<array<int,4>, array<int,8>> = 0
vector: v4 = v8 is legal, v4.size() now 8
array: a = b legal only for equal N; a size fixed at 4std::array<int,4> and std::array<int,8> are unrelated types, exactly as Chapter 18.5 §5 measured for any class parameterised on a value. You cannot assign one to the other, you cannot store both in the same container, and a function taking const std::array<int,4>& will not accept an eight-element array — it is a compile error, not a conversion.
For std::vector<int>, size is runtime data. v4 = v8 compiles and leaves v4 holding eight elements. One std::vector<int> parameter accepts every length.
This is the single most practically important difference after allocation, because it decides how functions are written. A function that should work on any length takes std::vector<int>&, or — better, and the subject of Module 20 — a pair of iterators or a std::span, which accepts both containers and a built-in array too. A function that genuinely requires exactly four elements can say so in its signature with std::array<int,4>, and the compiler will enforce it; that is a real benefit and it is the second of std::array's two claims.
7. What std::array Fixes About Built-In Arrays
std::array exists because the built-in array is a poor citizen, and the comparison is worth laying out because it explains every design choice in std::array.
A built-in array decays to a pointer at the slightest provocation — Chapter 18.3 §4 measured a const char[6] becoming a const char* when deduced by value — so it loses its length the moment it is passed anywhere. std::array is a class, and classes do not decay; it keeps its length through every function call.
A built-in array cannot be assigned or returned. std::array can be both, because the implicit copy operations of an aggregate do the obvious thing.
A built-in array has no size(), so the length has to travel alongside it in a separate parameter that nothing checks. std::array::size() is a compile-time constant with no storage cost.
A built-in array has no bounds-checked access. std::array::at() throws.
And std::array has iterators, so it works with every standard algorithm and with range-based for by name rather than by the std::begin dance.
What std::array does not fix is the thing it cannot: it still does not bounds-check operator[]. §11 covers that.
8. When to Use Which
The decision is short enough to state as three conditions.
Use std::array when the size is a compile-time constant and small. A 3-element coordinate, a 16-byte key, a fixed lookup table, a fixed-length protocol header. You get zero allocation, zero indirection, and the size checked by the type system.
Use std::array when allocation is forbidden or unavailable. Static initialisation before main, interrupt context, freestanding targets.
Use std::vector otherwise — which is to say, whenever the size is determined at run time, might change, is large, or is simply not known at the point where the code is written. That is the overwhelming majority of sequences in ordinary code, which is the content of the frozen purpose's claim.
The size threshold in the first condition is a judgement, and the reason for it is §3: an array is as big as its contents and lives wherever it is declared. A few hundred bytes as a local is unremarkable. A megabyte is a bug waiting for a deep call stack. As a rough working rule, past a few kilobytes prefer vector even for a fixed size, unless the no-allocation requirement is what put you on array in the first place.
9. The Comparison Table
std::array<T,N> | std::vector<T> | T[N] | |
|---|---|---|---|
| size fixed at compile time | yes | no | yes |
| size part of the type | yes (measured: not same type) | no | yes |
| object size | N * sizeof(T) (measured 4096 for 1024 ints) | 24 bytes (measured) | N * sizeof(T) |
| heap allocations | 0 (measured) | 1 per capacity change (measured 11 for 1000 pushes) | 0 |
| elements live | with the object | on the heap (measured) | with the object |
| contiguous | yes | yes | yes |
data() for C interop | yes (measured) | yes (measured) | it is a pointer |
| can grow | no | yes | no |
| decays to a pointer | no | no | yes |
| assignable / returnable | yes | yes | no |
has size() | yes, compile-time | yes | no |
| bounds-checked option | at() | at() | none |
| works with algorithms | yes | yes | with std::begin/end |
10. Initialisation, Briefly
std::array is an aggregate, so it follows the aggregate rules from Chapter 9.8 §3 rather than having constructors of its own, and that produces one genuine trap:
std::array<int,4> a; // elements UNINITIALISED - indeterminate values
std::array<int,4> b{}; // all four zero-initialised
std::array<int,4> c{1,2}; // {1, 2, 0, 0} - the rest are value-initialisedThe first line is the trap, and it is the same one built-in arrays have: the elements hold the indeterminate values Chapter 3.8 §3 defines, and reading one is undefined behaviour. Always write the braces.
std::vector has constructors and therefore no such trap:
std::vector<int> v; // empty, no allocation
std::vector<int> w(4); // four zero-initialised ints
std::vector<int> x{4}; // ONE element with the value 4
std::vector<int> y(4, 7); // four sevensThe difference between w(4) and x{4} is the braces-versus-parentheses hazard Chapter 3.7 §4 covers: the initialiser-list constructor wins whenever the braced contents can match it. std::vector<int> x{4} is a one-element vector, which has surprised everyone at least once.
11. Access, Checked and Unchecked
Both containers offer the same four ways in, and the distinction is the same for both.
operator[] is unchecked. An out-of-range index is undefined behaviour, and in a release build it will usually read or write whatever is at that address. at() is checked and throws std::out_of_range. front() and back() are unchecked and are undefined on an empty container — which is easy to hit with vector and impossible with a non-empty array.
The reason operator[] is not checked is the language's standing principle that you do not pay for what you did not ask for. A loop that has already established its bounds should not re-check them every iteration.
The practical habit: operator[] inside code that has established the bound, at() at a boundary where the index came from outside — a file, a user, a network message — and size() checked explicitly when neither applies. And build the test configuration with _LIBCPP_HARDENING_MODE or the equivalent, which turns operator[] into a checked access during development without changing the release build.
12. What to Actually Do
- Default to
std::vectorfor a sequence. Deviate for a reason you can name from §8. - Use
std::arraywhen the size is a compile-time constant and small, or when allocation is unavailable. - Call
reservewhen the final size is known. Measured: 11 allocations to 1. - Always brace-initialise a
std::array—std::array<int,4> a{}— because the unbraced form leaves it indeterminate. - Remember
vector<int> x{4}is one element, not four. Use parentheses for a count. - Pass
data()andsize()together to C interfaces, and never&v[0]on a possibly-empty vector. - Prefer
std::array<T,N>overT[N]in every new declaration: it does not decay, it is assignable, and it knows its length. - Use
at()at trust boundaries andoperator[]inside loops whose bounds you have already established.
13. Mistakes Worth Making Once
A large std::array as a local. std::array<double, 1'000'000> is an eight-megabyte stack frame. The crash is immediate and the cause is invisible until you remember §3.
std::array<int,4> a; without braces. Indeterminate elements, and the values are often zero by accident, so it passes its test and fails later.
std::vector<int> v{4}; One element. Then a loop over v.size() runs once and the bug is a wrong answer, not a crash.
Passing a std::array<int,8> where std::array<int,4> is wanted and being surprised it is a hard error. It is the same distinct-types rule Chapter 18.5 §5 measured, doing its job.
Taking &v[0] on an empty vector. Undefined behaviour that returns a plausible pointer. Use data().
Assuming std::vector shrinks when you erase. It does not; Chapter 19.3 §5 measures capacity staying at 10,000 after clear().
Using a std::vector member for a fixed three-element coordinate. A pointer chase and an allocation for twelve bytes that could have been in the object.
14. Interview Relevance
The frozen curriculum marks this chapter Critical, and it is the one container topic that comes up in essentially every interview.
"Why is std::vector the default container?" Contiguity, which gives O(1) indexing, cache-friendly traversal and C interop, plus a size that need not be known at compile time. The strong version of the answer mentions that its one weakness — reallocation — is removable with reserve, and quotes the 11-to-1 measurement.
"What is the difference between std::array and std::vector?" Fixed size in the type versus runtime size; stack or in-object versus heap; zero allocations versus one per capacity change; and array's size being part of its type so lengths cannot be mixed.
"What is the difference between std::array and a C array?" The five fixes in §7, and the one thing it does not fix.
"How big is a std::vector?" Twenty-four bytes on a 64-bit platform, three pointers, independent of the element count. Interviewers like this because it tests whether you distinguish the container object from its storage.
"When would you not use std::vector?" Fixed small size, no allocation permitted, or a guarantee from Chapter 19.1 §10's table that vector does not make — reference stability across insertion being the usual one.
"What does reserve do, and what does it not do?" Allocates capacity without creating elements. reserve(5) leaves size() at 0, so v[0] is still undefined behaviour; that is §5's and Chapter 19.3 §6's distinction between reserve and resize.
15. What You Should Be Able to Say Now
- That both containers are contiguous and that this is the property that matters.
- That
std::arrayadds zero bytes and zero allocations, measured. - That a
std::vectorobject is 24 bytes regardless of what it holds. - That
std::array's size is part of its type andstd::vector's is not, and what that means for function signatures. - That 1000 unreserved appends cost 11 allocations and 8,188 bytes, and
reservemakes it 1 and 4,000. - The five problems
std::arrayfixes about built-in arrays. - Why
std::array<int,4> a;is a bug andstd::vector<int> x{4}is one element. - The three conditions that select
std::arrayoverstd::vector.
16. Exercises
Exercise 1 — the sizes. Print sizeof for std::array<char,1>, std::array<char,100>, std::vector<char> and std::vector<std::string>. Explain why two of the four are equal.
Exercise 2 — count the allocations. Reproduce the counting allocator and confirm the 11-and-8188 figure. Then find the smallest n for which the unreserved path allocates more than twice the final payload.
Exercise 3 — the decay. Write a function taking int[4], print sizeof of the parameter inside it, and explain the number. Then do the same with std::array<int,4>.
Exercise 4 — one parameter, two lengths. Write a function that sums any-length sequences of int. Do it three ways: const std::vector<int>&, a pair of iterators, and a template on N for std::array. Note which of the three accepts all of a vector, an array and a C array.
Exercise 5 — the brace trap. Construct std::vector<int> four ways to hold exactly {4}, exactly four zeros, and four sevens. Then do it for std::vector<std::string> and explain why the hazard is milder there.
Exercise 6 — the stack limit. Find, by bisection, the largest std::array<char,N> your platform will accept as a local variable. Compare with the documented stack size.
Exercise 7 — interop. Write a C-style function taking const double* and a count, and call it with a std::vector<double>, a std::array<double,8> and a double[8]. Then try it with a std::deque<double> and explain the compile error.
17. Summary
- Both containers are contiguous, which gives O(1) indexing, linear traversal and C interoperability through
data(). - Measured:
std::arrayadds exactly zero overhead — 32 bytes for eightints, identical toint[8], and trivially copyable. - Measured: a
std::vectorobject is 24 bytes whatever it holds; the elements are elsewhere. - Measured:
std::arraymakes 0 heap allocations, and its elements sit 32 bytes from a stack local; avector's are in the heap region. - Measured: 1000 unreserved
push_backs = 11 allocations / 8,188 bytes; reserved = 1 / 4,000. - Measured:
std::array<int,4>andstd::array<int,8>are unrelated types, while onestd::vector<int>parameter takes every length. std::arrayfixes five things about built-in arrays — decay, assignment,size(),at(), iterators — and does not fix uncheckedoperator[].std::vectoris the default;std::arraywins on a compile-time constant small size, or where allocation is unavailable.std::array<int,4> a;is indeterminate andstd::vector<int> x{4}holds one element.
The one cost this chapter quoted without explaining is std::vector's growth: eleven allocations, 8,188 bytes, and an unstated number of element moves on the way to a thousand elements. Chapter 19.3 takes that apart completely — the exact capacity sequence the implementation uses, how many times each element is moved, what reserve removes and what it does not, and why clear() frees nothing at all. It is the one chapter in this module about a single container, and the reason is that std::vector is the container you will use most and its growth behaviour is the only part of it that can surprise you.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
