What an Iterator Actually Is
Every standard container - vector, string, map, set, list - stores its elements differently inside. A vector is a contiguous block, a map is a balanced tree, a list is linked nodes. Yet you can loop over all of them the same way. The thing that makes that possible is the iterator: a small object that "points at" one element and knows how to step to the next.
Think of an iterator as a generalized pointer. You get one from begin(), read the element it points to with *, and move it forward with ++. The pieces fit together like this:
v.begin() returns an iterator to the first element; *it gives you that element; ++it moves to the next. That trio - dereference, advance, compare - is the entire mental model.
begin(), end(), and the Half-Open Range
The other half of the picture is end(). Crucially, end() does not point at the last element - it points at the slot one past the last element. This is a deliberate "half-open" range [begin, end): begin is included, end is the stop signal.
That design makes the standard loop clean - you walk until the iterator equals end():
Note it != v.end(), not it < v.end(). Most container iterators (like map or list) don't support <, only == and !=, so != is the portable choice. And auto saves you from writing vector<int>::iterator by hand - the compiler deduces it.
The empty-container case falls out naturally: when a container is empty, begin() == end(), so the loop body never runs. No special-casing needed.
Never Dereference end()
The most common iterator bug is dereferencing end(). Since it points one past the last element, *v.end() reads memory that isn't yours - undefined behavior, which means a crash or silent garbage, not a friendly error:
vector<int> v = {1, 2, 3};
cout << *v.end(); // UNDEFINED BEHAVIOR - end() is not an element
The same trap hits search functions. std::find returns end() when it doesn't find the value, so you must check before dereferencing:
Always compare the returned iterator against end() before you dereference it. Forgetting this if is one of the most frequent sources of crashes in beginner STL code.
const, cbegin, and Reverse Iterators
Containers hand out different flavors of iterator depending on what you need:
begin()/end()- normal read/write iterators (*it = ...works).cbegin()/cend()-const_iterators; you can read through them but not modify the element.rbegin()/rend()- reverse iterators that walk back to front;++actually moves backward.
Reverse iterators are the clean way to loop in reverse without fiddly index math:
With reverse iterators you still write ++it to make progress - the iterator handles the "backwards" direction internally. Use cbegin()/cend() (or a const reference to the container) when a loop should only read, so the compiler stops you from accidentally writing.
Map Iterators Yield Pairs
Not every iterator is a thin wrapper over a pointer. A std::map iterator walks a tree, and dereferencing it gives you a std::pair of the key and value, accessed via ->first and ->second (just like a pointer, an iterator supports ->):
The range-based for loop is built directly on begin()/end(), so for plain forward iteration you'll usually reach for that instead. Explicit iterators earn their keep when you need a reverse walk, the position of an element, or to pass a range to an algorithm.
The Big Gotcha: Iterator Invalidation
This is the pitfall that bites everyone eventually. When you change a container's structure, existing iterators can become invalidated - they point at memory that's been freed or moved. Using one is undefined behavior.
For a vector, push_back may reallocate the whole buffer to grow it, invalidating every outstanding iterator. Erasing while looping is even more notorious - this is a classic crash:
vector<int> v = {1, 2, 3, 4};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0)
v.erase(it); // BUG - erase invalidates it, then ++it is UB
}
The fix is that erase returns a valid iterator to the element after the removed one. Advance only when you didn't erase:
Notice the for header has no ++it - the body decides whether to advance. (In real code, the erase-remove idiom or C++20's std::erase_if does this in one line.) The rule to remember: any operation that adds or removes elements may invalidate iterators, so don't hold onto an old iterator across such a change.
Next: Algorithms
Now that you can describe a range as a begin/end pair, you've unlocked the whole STL algorithms library. Functions like sort, find, count, and accumulate don't care which container you have - they operate on iterator ranges, so the same call works on a vector, an array, or a slice of one. Next we'll put those iterators to work and let the standard library do the looping for you.
Frequently Asked Questions
What is an iterator in C++?
An iterator is an object that points at an element inside a container and knows how to move to the next one. You get the first with container.begin() and a one-past-the-end marker with container.end(). Dereference it with *it to read or write the element, and advance it with ++it. Iterators are the common interface that lets STL algorithms work on any container.
What is the difference between an iterator and a pointer in C++?
For a vector or array, an iterator behaves almost exactly like a pointer - you dereference with *, advance with ++, and compare with ==/!=. But an iterator is a concept, not necessarily a raw pointer: a map or list iterator walks a tree or linked nodes, so it is a class type that overloads * and ++. Pointers are one kind of iterator; iterators generalize the idea to every container.
What causes iterator invalidation in C++?
Modifying a container's structure can leave existing iterators pointing at freed or moved memory. For a vector, push_back may reallocate and invalidate all iterators; erase invalidates iterators at and after the removed element. Using an invalidated iterator is undefined behavior. Use the iterator that erase returns, or reserve capacity up front, to stay safe.