Foundations · Module 3 · Lesson 3 of 10
std::array and std::vector
Replace raw arrays with containers that know their size, copy as values and offer checked access: std::array as a zero-overhead wrapper, and std::vector as a small object that owns a contiguous heap buffer.
By the end you can
- Compare std::array, std::vector and a C array in size, copying and checking
- Describe how a vector is laid out: a small object plus a heap buffer
- Choose between operator[] and at() and say what each does on a bad index
- Predict the contents produced by the parenthesis and brace forms of a vector constructor
Builds on Out-of-Bounds Access and Undefined Behavior, Passing by Value, by Reference and by Const Reference.
01 · A question first
A C array forgets its own length, cannot be copied with an equals sign, and cannot grow. How does std::vector fix all three and still keep the speed of an array? And why does sizeof of a vector holding a million elements print 24?
Make a guess, then step through the trace below to check it.
02 · Watch it run
std::vector<int> v = {10, 20, 30};int a = v[1];std::cout << sizeof(v);int b = v.at(5);- part 0v: 24 bytes(looked at in this step)
- part 1size: 3
- part 2heap[0]: 10(looked at in this step)
- part 3heap[1]: 20(looked at in this step)
- part 4heap[2]: 30(looked at in this step)
The vector object v lives in the stack frame and is small: three pointers, 24 bytes on a 64-bit machine. It does not contain the elements. They sit in a separate buffer on the heap, side by side, allocated when v was built from the initializer list. The cells show v, its size, and the three heap slots.
Machine work: allocate a heap buffer for 3 ints, fill the slots, set the three pointers
03 · The idea
Start with the problem
The raw array of lesson 1 has three weaknesses. It forgets its own length as soon as you pass it to a function. It cannot be copied or compared with = and ==. And its size is fixed when the program is compiled. The standard library fixes these with two containers that keep the contiguous layout you already understand: std::array for a fixed size, and std::vector for a size that changes while the program runs.
A word on notation: the type in angle brackets, as in std::vector<int>, is the element type. Read it as "a vector of int". The mechanism behind it, templates, comes in a later module.
How the idea works
std::array. It lives in the header <array> and wraps a C array in a proper type:
#include <array>
std::array<int, 3> a = {10, 20, 30};
std::cout << a.size(); // 3
a[1] = 99; // unchecked, like a C array
a.at(1) = 99; // checked
std::array<int, 3> b = a; // copies all three elementsIt knows its size, it can be copied, assigned and compared by value, and it works with range-based loops. The size is part of the type, so it must be a compile-time constant.
std::vector. It lives in <vector> and can change size while the program runs:
#include <vector>
std::vector<int> v = {10, 20, 30};
v.push_back(40); // now four elements
v.pop_back(); // back to three
std::cout << v.size(); // 3Besides push_back and pop_back, you will use size(), empty(), clear(), front() and back(), and insert and erase for the middle.
Behind the code: what a vector is. The trace shows it. The vector object in the stack frame is small, typically three pointers: one to the first element, one past the last element, and one past the end of the allocated buffer. That makes 24 bytes on a 64-bit system, whether the vector holds 3 elements or 3 million (step 3). The elements live in a separate, contiguous buffer on the heap, a region of memory that, unlike the stack, is not tied to function calls (lesson 9 explains it in detail). Indexing v[i] is the same arithmetic as for an array, preceded by one load of the begin pointer (step 2). Because the elements are in a heap buffer, the size can change when the program runs, which is the subject of lesson 4. A std::array has no heap buffer and no extra fields: it is exactly the bytes of the elements.
Copying. Copying a vector gives the copy its own buffer and copies every element. This is value semantics, as in lesson 3 of module 1: afterwards the two vectors are independent. It also means that passing a vector by value copies everything, so pass it by const reference, as lesson 7 of module 2 advised.
Checked and unchecked access. The expression v[i] does no checking, so a bad index is the undefined behavior of the last lesson. The call v.at(i) compares i with the size and throws a std::out_of_range exception if it fails (step 4). An exception that nobody catches ends the program with a message, which is much better than silent corruption. Exceptions are covered in the Core level.
Automatic cleanup. When a vector goes out of scope, its destructor releases the heap buffer. You never write delete for a vector, and it cannot leak. Lesson 9 shows what manual allocation looks like, so that you can appreciate why this matters.
Why it is correct
Bundling the pointer with the size and capacity in one object fixes the forgotten-length problem at its root, and the container maintains its own rules, such as keeping the three pointers in order and the elements between the first two always valid. The contiguous layout is kept, so everything from lesson 1 still holds: constant-time indexing, and loops that the cache handles well. Ownership is simple: one vector owns one buffer, a copy makes a new buffer, and the end of its scope frees it. These properties are why C++ guidance says to use std::vector unless you have a reason not to.
What it costs
std::array. Nothing extra. It has the same size and speed as a C array. It also lives wherever it is declared, so a large std::array local, such as 12 MB of integers, overflows an 8 MB stack as lesson 6 described.
std::vector. The object costs 24 bytes, and the buffer costs one heap allocation, typically tens to a few hundred nanoseconds. Each access loads the begin pointer first, which is usually cached. Inserting or erasing in the middle shifts all later elements, so erasing at the front of a vector of a million integers moves about 4 MB. Appending at the end is cheap on average, as lesson 4 shows.
Copies. Copying a vector of a million integers moves 4 MB, roughly half a millisecond, plus an allocation. Copying one on every function call or loop iteration adds up quickly, so pass by const reference.
Checking. The call at() adds a comparison and a branch to each access, a small cost that is often worth paying, and [] stays available for code you have proved correct.
Pitfalls and variations
Parentheses versus braces. std::vector<int> v(5) creates five zeros, but std::vector<int> v{5} creates one element with the value 5. Likewise v(5, 1) is five ones, and v{5, 1} is the two elements 5 and 1. The braces form means "these are the elements".
Reallocation invalidates. Appending may move the buffer, and then every pointer, reference and iterator into the old buffer dangles. Lesson 4 explains when it happens.
The index at the size. v[v.size()] is one past the end, the usual off-by-one error.
Signed and unsigned. v.size() returns an unsigned type, so for (int i = 0; i < v.size(); ++i) provokes the comparison warning of lesson 4. Use a range-based loop, or std::size_t for the index.
Accidental copies. auto x = v; copies the vector, and so does a by-value parameter. Use const auto& when you only need to look.
The odd one out. std::vector<bool> packs bits and does not behave like the other vectors, so avoid it in general code.
Variations. std::span, from C++20, is a non-owning view of a run of contiguous elements, and works with both containers and C arrays. Other containers, such as std::deque and std::list, arrive in the Core level. A vector of vectors is not one contiguous block, because each inner vector has its own buffer.
Where it is used
std::vector is the default container of C++: lists of items, buffers of data, results of a computation, the entities of a game. The standard algorithms from the Core level operate on it, and most other containers exist for special cases where it is not the best fit.
The next lesson, "How a Vector Grows: Size, Capacity and Reallocation", explains what happens inside push_back when the buffer is full, and why appending is cheap on average even though some appends are expensive.
04 · Prove it
Question 1 of 4 · answer from memory
What does `sizeof(v)` report for a `std::vector<int> v` holding a million elements, on a typical 64-bit libstdc++ build?
05 · Practice
Asked in interviews
Something wrong with this lesson? Report a problem