C/C++, Data Structures
Vectors in C++: A Complete Guide

A std::vector is the C++ Standard Template Library container for a dynamic array: a sequence of one element type stored in contiguous memory that grows and shrinks at runtime. You get random access in O(1), append in amortized O(1), and automatic memory management, with no manual new/delete and no fixed size to guess up front. This guide walks the full surface area: declaration, the add and remove operations, iterators, size versus capacity, 2D vectors, custom types, and the complexity of every operation, with code that compiles.
If a graded assignment is what brought you here, our C++ assignment help service pairs you with a developer who works in the STL daily, matches your compiler and the autograder's output format, and walks you through the code so you can defend it. You pay 50% to start and the second half only after the code runs on your machine.
What is a vector in C++
A vector is a sequence container that holds elements of one type in contiguous memory and resizes itself automatically. It lives in the <vector> header and sits inside the std namespace, so the full name is std::vector. Where a built-in array fixes its length at compile time, a vector decides its length at runtime and changes it whenever you add or remove an element.
Contiguous storage is the property that makes a vector fast. Because every element sits next to the previous one in memory, indexing is a single pointer offset, and the CPU cache prefetches neighbors on every read. That layout is also why a vector can hand a plain pointer to its data through data(), which lets it interoperate with C APIs and older array-based code.
Being part of the STL means the container ships with the compiler, carries a tested implementation, and exposes the same interface across every standard-library vendor. You write the logic; the container handles allocation, growth, and cleanup.
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums = {1, 2, 3}; // a vector of three ints
nums.push_back(4); // grows to {1, 2, 3, 4}
for (int n : nums) {
std::cout << n << ' '; // 1 2 3 4
}
std::cout << '\n';
return 0;
}

Vector vs array in C++: 5 differences that matter
The decision between a vector and a raw array comes down to whether the size is known and fixed. An array fixes its length at compile time and forgets it; a vector tracks and changes its length at runtime. Five differences drive the choice in practice.
| Aspect | Raw array | std::vector |
| --- | --- | --- |
| Size | Fixed at compile time | Changes at runtime |
| Knows its length | No, you track it separately | Yes, via size() |
| Bounds checking | None | at() throws std::out_of_range |
| Memory management | Manual with new[] / delete[] | Automatic |
| Passing to functions | Decays to a pointer, loses length | Carries its length |
Reach for a raw array when the count is a compile-time constant and performance is critical down to the last byte, such as a fixed lookup table. Reach for a vector everywhere the count varies: user input of unknown length, rows read from a file, or a buffer that grows as a program runs. The vector removes the two array bugs that cost the most autograder points, off-by-one writes past the end and forgotten delete[] leaks, because it bounds-checks through at() and frees its own memory.
For the wider container picture, including when std::array, std::list, or std::map fits a problem better than a vector, see our introduction to the Standard Template Library in C++.
Declaring and initializing a vector in C++
Include <vector>, then name the element type inside angle brackets. The element type goes between < and >; the variable name follows. Five initialization forms cover almost every case.
#include <string>
#include <vector>
std::vector<int> empty; // 1. empty vector
std::vector<int> ages(5, 0); // 2. five ints, all 0
std::vector<int> fib = {0, 1, 1, 2, 3, 5, 8}; // 3. from a list of values
std::vector<int> copy(fib); // 4. copy of another vector
std::vector<std::string> words(3); // 5. three empty strings
- Empty: no elements yet. Use this when items arrive later through
push_back. - Size and fill value:
ages(5, 0)builds fiveintslots, each set to0. The first argument is the count, the second is the value copied into every slot. - Initializer list: the braces fill the vector with the listed values in order, the form used most in assignment code.
- Copy:
copy(fib)duplicates every element offibinto a new, independent vector. - Size only:
words(3)makes three default-constructed elements, three empty strings here, ready to assign into by index.
A common trap: std::vector<int> v(5) makes five zeros, while std::vector<int> v{5} makes one element holding the value 5. Parentheses pass a count; braces pass values.
Adding elements: push_back, emplace_back, and insert
Three methods add elements, and they differ in where the element lands and how it gets built. The first sentence to memorize: push_back and emplace_back append to the end in amortized O(1), while insert places an element anywhere and pays O(n) for the shift.
#include <string>
#include <vector>
std::vector<int> scores = {80, 85, 90};
scores.push_back(95); // append -> {80, 85, 90, 95}
std::vector<std::string> names = {"Alice", "Bob"};
names.emplace_back("Charlie"); // build in place -> {Alice, Bob, Charlie}
scores.insert(scores.begin() + 1, 82); // insert at index 1 -> {80, 82, 85, 90, 95}
push_back takes an existing object and copies or moves it into the vector. emplace_back forwards its arguments straight to the element's constructor and builds the object inside the vector's storage, which skips one temporary. For an int the two compile to the same thing. For a type with a multi-argument constructor, emplace_back(args...) is the leaner call because it never builds a throwaway object first.
insert takes an iterator marking the position, not an index, which is why scores.begin() + 1 appears rather than 1. Every element from that position onward shifts one slot to the right, so inserting near the front of a large vector is expensive. When you insert often at arbitrary positions, a std::list or std::deque fits better than a vector.
Accessing elements: indexing, at, front, and back
Four access paths read or write a vector's elements, and they split on one question: do you want bounds checking? The subscript operator [] does not check; at() does.
#include <vector>
std::vector<int> v = {10, 20, 30, 40};
int a = v[2]; // 30, no bounds check (fast, unsafe)
int b = v.at(2); // 30, throws std::out_of_range if out of bounds
int first = v.front(); // 10, first element
int last = v.back(); // 40, last element
v[2] returns the element at index 2 with zero overhead and zero safety: an out-of-range index is undefined behavior, which often reads garbage or crashes. v.at(2) does the same read but throws std::out_of_range on a bad index, which turns a silent memory bug into a catchable exception. Use [] inside a loop you already bounded with size(); use at() when the index comes from input you do not control. front() and back() return the first and last elements directly and assume the vector is not empty, so guard them with an empty() check.
Removing elements: pop_back, erase, and the erase-remove idiom
Removal mirrors insertion: dropping the last element is cheap, removing from the middle is not. pop_back() removes the last element in O(1). erase() removes at a position and shifts everything after it, costing O(n).
#include <algorithm>
#include <vector>
std::vector<int> data = {10, 20, 30, 40, 50};
data.pop_back(); // {10, 20, 30, 40}
data.erase(data.begin() + 1); // remove index 1 -> {10, 30, 40}
// Remove every element equal to 30 (erase-remove idiom)
data.erase(std::remove(data.begin(), data.end(), 30), data.end());
// -> {10, 40}
erase(iterator) deletes one element; erase(first, last) deletes a half-open range. To delete by value, the erase-remove idiom is the standard tool. std::remove from <algorithm> does not actually shorten the vector. It shuffles the elements you want to keep to the front and returns an iterator marking the new logical end, then erase trims the tail. Skipping the erase half is a frequent bug: the size never changes and stale values linger past the new end. C++20 simplifies this to a single std::erase(data, 30) free function.
Iterating through a vector: 4 ways to traverse
Four loop styles walk a vector, ordered from most explicit to most modern. Pick the range-based for for plain reads; reach for an index or an iterator when you need the position or want to modify during traversal.
A counted for loop uses the index, which you need when the position itself matters:
#include <vector>
std::vector<int> nums = {1, 2, 3, 4, 5};
for (std::size_t i = 0; i < nums.size(); ++i) {
nums[i] *= 2; // i is available if you need the index
}
The range-based for loop, added in C++11, drops the index entirely. Take the element by const reference to read, or by plain reference to modify:
#include <string>
#include <vector>
std::vector<std::string> fruits = {"Apple", "Banana", "Orange"};
for (const std::string& fruit : fruits) {
// read each fruit without copying it
}
An explicit iterator gives the same traversal with full control, useful when you call algorithms that take iterator pairs:
#include <vector>
std::vector<double> prices = {10.99, 20.49, 5.99};
for (std::vector<double>::iterator it = prices.begin(); it != prices.end(); ++it) {
*it *= 1.1; // dereference with *it to read or write
}
The auto keyword trims the iterator's long type name, the form most C++ code uses today:
for (auto it = prices.begin(); it != prices.end(); ++it) {
// *it is the current element
}
One rule guards all four: do not call push_back or erase inside a range-based or iterator loop, because a reallocation or shift invalidates the iterator you are holding. The counted index loop survives a push_back as long as you re-read size() each pass.
Size vs capacity: how a vector grows
Size and capacity are different numbers, and the gap between them is what keeps push_back fast. size() is the count of elements stored. capacity() is the count of slots already allocated, always greater than or equal to size().
A vector over-allocates on purpose. When you push_back and the size is still below capacity, the new element drops into a reserved slot in O(1). When size reaches capacity, the vector allocates a larger block, typically 1.5x or 2x the old one, moves every existing element into it, and frees the old block. That move is O(n), but because it happens rarely and the capacity grows geometrically, the cost averages out to amortized O(1) per push_back.

reserve(n) is the optimization that pays off most. When you know roughly how many elements are coming, reserving that capacity up front does all the allocation once and skips every intermediate reallocation:
#include <vector>
std::vector<int> v;
v.reserve(1000); // allocate room for 1000 now
for (int i = 0; i < 1000; ++i) {
v.push_back(i); // no reallocation happens in this loop
}
Three more sizing methods round out the set. resize(n) changes the element count, default-constructing new elements or dropping trailing ones. shrink_to_fit() releases capacity above the current size to reclaim memory. clear() sets size to zero but leaves capacity untouched, so the freed slots stay reserved for reuse.
Reallocation and iterator invalidation
A reallocation moves the vector's elements to a new memory block, which invalidates every pointer, reference, and iterator into the old block. This is the single most common source of crashes in vector code, and the autograder catches it as a use-after-free.
#include <vector>
std::vector<int> v = {1, 2, 3};
int& ref = v[0]; // reference to the first element
v.push_back(4); // MAY reallocate, moving the data
// ref now MAY point at freed memory -> undefined behavior
The fix is one of two moves. Call reserve() before a known sequence of insertions so no reallocation occurs, which keeps existing references valid. Or re-fetch the pointer, reference, or iterator after any operation that can grow the vector. The [] index, by contrast, stays valid across a reallocation because it recomputes the address from the current data pointer every time, which is one more reason indexing is safer than caching a reference across an insert.
Common vector algorithms from the STL
The <algorithm> header turns a vector into the input for dozens of generic functions, each taking an iterator pair. The two you use most are std::sort for ordering and std::find for searching.
#include <algorithm>
#include <vector>
std::vector<int> nums = {5, 2, 8, 1, 9};
std::sort(nums.begin(), nums.end()); // {1, 2, 5, 8, 9}, O(n log n)
auto it = std::find(nums.begin(), nums.end(), 8);
if (it != nums.end()) {
// found: *it == 8, and (it - nums.begin()) is the index
} else {
// 8 is not in the vector
}
std::sort orders the range ascending in O(n log n) by default; pass std::greater<int>{} as a third argument for descending, or a lambda to sort on a custom key. std::find returns an iterator to the first match, or end() when no element matches, so the it != end() check is mandatory before you dereference. Other regulars include std::reverse, std::max_element, std::min_element, std::accumulate (from <numeric>), and std::count. Each takes the same begin()/end() pair, which is why mastering one transfers to all of them.
Vectors of custom types and comparators
A vector holds any copyable type, including your own classes and structs, not only int and std::string. Declare the element type the same way and the container handles the rest.
#include <string>
#include <utility> // std::move
#include <vector>
struct Student {
std::string name;
int score;
Student(std::string n, int s) : name(std::move(n)), score(s) {}
};
int main() {
std::vector<Student> roster;
roster.push_back({"Alice", 92}); // builds a temporary, then moves it in
roster.emplace_back("Bob", 87); // forwards the args, builds in place
return 0;
}
To sort a vector of a custom type, give std::sort a comparator, because the compiler does not know which field defines order. A lambda is the cleanest way to name the key:
#include <algorithm>
#include <vector>
// Sort the roster from highest score to lowest.
std::sort(roster.begin(), roster.end(),
[](const Student& a, const Student& b) {
return a.score > b.score;
});
For an STL algorithm such as std::find that compares for equality, overload operator== on the type so the container knows when two objects match. Defining ordering and equality once on the type lets every algorithm in <algorithm> operate on your vector. If the elements own resources or you are reaching for pointers to share objects between containers, store a smart pointer in C++ such as std::unique_ptr inside the vector instead of a raw pointer, so the container's automatic cleanup also frees the pointees.
Passing vectors to functions
How you pass a vector decides whether the function copies the whole container or works on the caller's data. Pass by const reference to read, by reference to modify, and by value only when the function genuinely needs its own copy.
#include <vector>
// Read only: no copy, cannot modify the caller's vector.
int sum(const std::vector<int>& v) {
int total = 0;
for (int x : v) total += x;
return total;
}
// Modify the caller's vector in place: no copy.
void doubleAll(std::vector<int>& v) {
for (int& x : v) x *= 2;
}
// Independent copy: changes here do not touch the caller.
void experiment(std::vector<int> v) {
v.push_back(99); // affects the local copy only
}
Passing by value copies every element, which is wasteful for anything beyond a handful of items, so const std::vector<int>& is the default for read-only parameters. Returning a vector from a function is cheap by contrast: the compiler moves it out or elides the copy entirely, so std::vector<int> build(); carries no penalty and is the idiomatic way to produce a vector.
2D vectors: a vector of vectors
A 2D vector is a vector whose elements are themselves vectors, which models a grid, matrix, or table with dynamic rows and columns. The outer vector holds the rows; each inner vector holds one row's columns.
#include <iostream>
#include <vector>
int main() {
int rows = 3, cols = 4;
// 3 rows, 4 columns, every cell initialized to 0.
std::vector<std::vector<int>> grid(rows, std::vector<int>(cols, 0));
grid[1][2] = 7; // set row 1, column 2
for (const auto& row : grid) {
for (int cell : row) {
std::cout << cell << ' ';
}
std::cout << '\n';
}
return 0;
}
The constructor grid(rows, std::vector<int>(cols, 0)) repeats the inner vector rows times, building a rectangle filled with zeros. Index it with grid[r][c], row first. Because each inner vector sizes itself, rows can hold different lengths, which gives a jagged structure that a fixed 2D array cannot. 2D vectors fit matrix math, dynamic-programming tables, pixel grids in image processing, and any board or map whose dimensions are not known until runtime.
Vector performance and the std::vector<bool> caveat
A vector trades cheap ends for expensive middles, and knowing the cost of each operation prevents the slow code that passes correctness checks but fails performance ones. The table sums up the time complexity of every core operation.
| Operation | Method | Complexity |
| --- | --- | --- |
| Access by index | v[i], v.at(i) | O(1) |
| Append at end | push_back, emplace_back | Amortized O(1) |
| Remove last | pop_back | O(1) |
| Insert or erase in middle | insert, erase | O(n) |
| Find a value | std::find | O(n) |
| Sort | std::sort | O(n log n) |
Three habits keep vector code fast. Call reserve() when the final size is predictable, to collapse many reallocations into one. Switch to std::list or std::deque when the workload inserts and removes at the front or middle constantly, since those containers pay O(1) where a vector pays O(n). And pass large objects by reference, or store them through a smart pointer, to avoid copying every element on assignment.
One specialization breaks the usual rules: std::vector<bool>. The standard packs its elements into bits, eight per byte, to save memory, so it is not a normal vector of bool. The catch is that operator[] returns a proxy object rather than a real bool&, which means auto& b = bits[0]; does not compile and &bits[0] does not give a usable pointer. When you need genuine bool references or pointer access, use std::vector<char> or std::deque<bool> instead.
#include <vector>
std::vector<bool> flags = {true, false, true, true}; // bit-packed, special rules
Worked example: reading numbers until a sentinel
A classic assignment reads an unknown count of numbers from input, which is the exact case a vector handles and a fixed array cannot. Push each value as it arrives; the vector grows to fit.
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
int input;
std::cout << "Enter numbers (-1 to stop): ";
while (std::cin >> input && input != -1) {
numbers.push_back(input); // grows on demand
}
int total = 0;
for (int n : numbers) total += n;
std::cout << "Count: " << numbers.size() << '\n';
std::cout << "Sum: " << total << '\n';
return 0;
}
The original WordPress version of this post shipped several snippets that do not compile: empty #include lines with no header named, std::vector declarations with no element type, and curly typographic quotes around string literals where the compiler needs straight quotes. Every example above carries the real headers it needs, the element type inside the angle brackets, and plain quotes, so each one builds as written.
Frequently asked questions
What is a vector in C++?
A vector is a sequence container in the C++ Standard Template Library that stores elements of one type in contiguous memory and resizes itself as you add or remove items. It behaves like a dynamic array: random access is O(1), push_back is amortized O(1), and the container manages its own memory. Include <vector> and declare it as std::vector<int> nums; to use it.
What is the difference between a vector and an array in C++?
A raw array has a fixed size set at compile time and never tracks its own length. A std::vector grows and shrinks at runtime, knows its size through size(), bounds-checks with at(), and frees its memory automatically. Use an array when the size is a known constant; use a vector when the count changes or comes from input.
What is the difference between push_back and emplace_back?
push_back takes an already-built object and copies or moves it into the vector. emplace_back forwards its arguments to the element constructor and builds the object directly in the vector's storage, skipping one temporary. For an int the two are identical; for a struct with a multi-argument constructor, emplace_back avoids constructing a temporary object first.
What is the difference between size and capacity in a vector?
size() is the number of elements currently stored. capacity() is the number of slots already allocated, which is greater than or equal to size. A vector over-allocates so most push_back calls reuse existing room; when size reaches capacity, the vector allocates a larger block, moves every element, and frees the old block.
How do I remove an element from a vector in C++?
Use pop_back() to drop the last element in O(1). Use erase(iterator) to remove an element at a position, which shifts every later element left and costs O(n). To remove by value, combine std::remove with erase: v.erase(std::remove(v.begin(), v.end(), value), v.end()), known as the erase-remove idiom.
Why does a vector pointer or iterator become invalid after push_back?
When push_back forces a reallocation, the vector moves its elements to a new memory block and frees the old one. Any pointer, reference, or iterator into the old block then points at freed memory. Call reserve() before a loop of push_back calls, or re-fetch the iterator after any insertion, to avoid using a dangling reference.
Should I pass a vector by value or by reference to a function?
Pass by const reference (const std::vector<int>& v) when the function only reads, which avoids copying the whole container. Pass by non-const reference (std::vector<int>& v) when the function modifies the caller's vector. Pass by value only when the function needs its own independent copy, since copying a vector duplicates every element.
How do I declare a 2D vector in C++?
A 2D vector is a vector of vectors. Declare std::vector<std::vector<int>> grid; for an empty one, or std::vector<std::vector<int>> grid(rows, std::vector<int>(cols, 0)); to build a rows-by-cols grid filled with zeros. Access an element with grid[r][c]. Each inner vector can hold a different length, which makes jagged rows possible.
Where to go next
Vectors are the entry point to the wider STL, and the same iterator and template ideas carry into the rest of the library. To see how containers and algorithms compose, read the introduction to the Standard Template Library in C++. To understand the generic machinery that lets one std::vector hold any type, study templates in C++. And when a deadline is closing in on a graded C++ assignment, our C++ assignment help team builds the code, matches the autograder's output format, and gives you a short walkthrough so you can answer questions on it. Pricing starts at $29, you pay 50% to begin and 50% after the code runs on your machine, and every brief is covered by an NDA.
Related articles
- C/C++
Min Heap and Max Heap in C++
Build min heaps and max heaps in C++ with std::priority_queue and the STL heap algorithms, plus array math, heapify, heap sort, and worked examples.
Sep 19, 2023
- C/C++
C++ Programming: A Complete Introduction
Learn C++ from the ground up: how it compiles, its core data types, functions, and the four pillars of object-oriented programming, with runnable code.
Mar 30, 2023
- C/C++
Iterating Over a std::map in C++
Six ways to iterate over a std::map in C++, from structured bindings to reverse iterators, with working code, complexity notes, and the bugs that trip up students.
Jul 10, 2023


