std::vector
std::vector is the container to use when the number of elements is not known when the program is written. It owns a contiguous array on the heap, grows it when push_back runs out of room, and destroys the elements when it dies.
The heap part is invisible because the class manages it: no new, no delete, no length variable. What is visible is the difference between size and capacity, which decides how often the buffer is reallocated, and that is the part worth measuring.
Create, append, index
An empty vector has size 0 and no elements. push_back appends a copy of its argument and grows the buffer when needed, and size() counts the elements, not the bytes.
The program appends three ints and then walks the vector with an index. operator[] does no bounds check, which is why the loop condition uses size(): the index is checked by the loop, and the access stays a plain memory read.
A vector can also be built from an initialiser list, as std::vector<int> values{10, 20, 30};, which is what the last section of this lesson uses. Both forms give the same container.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values;
values.push_back(10);
values.push_back(20);
values.push_back(30);
std::cout << "size: " << values.size() << '\n';
for (std::size_t i = 0; i < values.size(); ++i) {
std::cout << values[i] << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra create.cpp -o create
./create
size: 3
10 20 30
size against capacity
capacity() is the number of elements the current buffer can hold. When push_back needs more, the vector allocates a bigger buffer, moves or copies every element into it and frees the old one; capacity then jumps, usually by doubling.
The program prints size and capacity after each append. The capacity goes 1, 2, 4, 8, 16, and the reallocations happen at 1, 3, 5 and 9 elements. Everything before a reallocation is cheap; the reallocation itself copies the whole vector.
That growth pattern is what makes push_back in a loop amortised constant time. A vector that knows the final count can skip the reallocations entirely, which is the next section.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values;
for (int i = 0; i < 9; ++i) {
values.push_back(i);
std::cout << values.size() << '/' << values.capacity() << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra grow.cpp -o grow
./grow
1/1 2/2 3/4 4/4 5/8 6/8 7/8 8/8 9/16
reserve
reserve(n) allocates room for n elements without changing the size. The following push_back calls then stay inside the buffer and no element is moved.
The program reserves 9 places and appends 9 values: size and capacity are both 9, and one allocation happened instead of four. The elements are still constructed one by one; reserve changes the buffer, not the size.
Reserve when the count is known or can be estimated. It is an optimisation, not a correctness tool: code must work without it, and a reserve that is too large wastes memory for the whole life of the vector.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values;
values.reserve(9);
for (int i = 0; i < 9; ++i) {
values.push_back(i);
}
std::cout << values.size() << '/' << values.capacity() << '\n';
}
g++ -std=c++23 -Wall -Wextra reserve.cpp -o reserve
./reserve
9/9
erase and insert
erase removes an element and shifts everything after it one place left; insert does the opposite and shifts right. Both take iterators rather than indices, so the position is values.begin() + 1 for the second element.
The program removes 20 from the middle and inserts 5 at the front. The result is 5 10 30 40 50: the positions of the later elements changed, which is why an index saved before the edit is not valid after it.
Both operations are linear in the number of elements after the position. Removing from the end is cheap; removing from the front of a large vector is a reason to look at std::deque in lesson 056.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values{10, 20, 30, 40, 50};
values.erase(values.begin() + 1);
values.insert(values.begin(), 5);
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra erase.cpp -o erase
./erase
5 10 30 40 50
A vector holds one type
Every element of a vector has the same type, fixed when the vector is declared. push_back takes that type, and a string literal cannot be converted to an int, so the call has no matching overload.
The message names the member and the argument type and lists the candidates it considered. That is the compiler saying the element type is int while the argument is a const char array, and the fix is a vector<std::string> or a different argument.
The same message appears for any mismatched argument, including a double into a vector<int>, which would narrow. Conversions that lose information are exactly the ones the standard library refuses to do silently.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values;
values.push_back("text");
std::cout << values.size() << '\n';
}
g++ -std=c++23 -Wall -Wextra wrong_type.cpp -o wrong_type
wrong_type.cpp: In function 'int main()':
wrong_type.cpp:7:21: error: no matching function for call to 'push_back(const char [5])'
7 | values.push_back("text");
| ~~~~~~~~~~~~~~~~^~~~~~~~
wrong_type.cpp:7:21: note: there are 2 candidates
In file included from /usr/include/c++/15.2.0/vector:68,
from wrong_type.cpp:2:
/usr/include/c++/15.2.0/bits/stl_vector.h:1416:7: note: candidate 1: 'constexpr void std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::push_back(const value_type&) [with _Tp = int; _Alloc = std::allocator<int>; value_type = int]' (near match)
1416 | push_back(const value_type& __x)
| ^~~~~~~~~
/usr/include/c++/15.2.0/bits/stl_vector.h:1416:7: note: conversion of argument 1 would be ill-formed:
wrong_type.cpp:7:22: error: invalid conversion from 'const char*' to 'std::vector<int>::value_type' {aka 'int'} [-fpermissive]
7 | values.push_back("text");
| ^~~~~~
| |
| const char*
/usr/include/c++/15.2.0/bits/stl_vector.h:1433:7: note: candidate 2: 'constexpr void std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::push_back(value_type&&) [with _Tp = int; _Alloc = std::allocator<int>; value_type = int]' (near match)
1433 | push_back(value_type&& __x)
| ^~~~~~~~~
/usr/include/c++/15.2.0/bits/stl_vector.h:1433:7: note: conversion of argument 1 would be ill-formed:
wrong_type.cpp:7:22: error: invalid conversion from 'const char*' to 'std::vector<int>::value_type' {aka 'int'} [-fpermissive]
7 | values.push_back("text");
| ^~~~~~
| |
| const char*
Exercises
- Print the elements of a vector in reverse with a loop that counts down from size() - 1. What happens when the vector is empty?
- Reserve 100 places and push 100 values, then print size and capacity. How much memory did the vector ask for beyond the elements?
- Replace erase with a loop that builds a new vector without the element. Which version is easier to reason about for a large vector?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a
std::vectorown?Answer
A contiguous array on the heap
The vector allocates one block, keeps the elements in it and frees it in its destructor.
-
What is the difference between size and capacity?
Answer
size counts the elements, capacity the room allocated
The capacity is the number of elements the block can hold before the next reallocation.
-
What does
push_backdo when the vector is full?Answer
It allocates a larger block and moves the elements
The growth policy of the lesson doubles the capacity and transfers the elements.
-
What does
reserve(n)change?Answer
The capacity, without changing the size
Reserve asks for room ahead of time, which is why the program prints 9/9.
-
What does this program print?
#include <iostream> #include <vector> int main() { std::vector<int> values; values.push_back(10); values.push_back(20); values.push_back(30); std::cout << "size: " << values.size() << " / " << values[0] << ' ' << values[1] << ' ' << values[2]; }Answer
size: 3 / 10 20 30
Three values are pushed, and the program prints the size and the elements in order.
-
What does this program print after the ninth push_back?
#include <iostream> #include <vector> int main() { std::vector<int> values; for (int i = 1; i <= 9; ++i) values.push_back(i); std::cout << values.size() << '/' << values.capacity(); }Answer
9/16
The ninth element does not fit in eight, so the capacity doubles to sixteen.
-
What does this program print?
#include <iostream> #include <vector> int main() { std::vector<int> values; values.reserve(9); for (int i = 1; i <= 9; ++i) values.push_back(i); std::cout << values.size() << '/' << values.capacity(); }Answer
9/9
The reserve gives room for nine, so no growth happens while the nine values are pushed.
-
What does this program print?
#include <iostream> #include <vector> int main() { std::vector<int> values{5, 10, 20, 30, 40, 50}; values.erase(values.begin() + 2); for (std::size_t i = 0; i < values.size(); ++i) std::cout << (i ? " " : "") << values[i]; }Answer
5 10 30 40 50
The element at the erased position is removed, and the rest keep their order.
-
What does pushing a string into a
std::vector<int>produce?Answer
no matching function for call to 'push_back(const char[5])'
The vector stores one type, and a literal does not convert to int.
-
What happens to iterators when a vector reallocates?
Answer
They are invalidated because the block moved
The elements are copied or moved to a new address, and the old addresses no longer refer to them.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev