CodeGym /Courses /C++ SELF /Iterators vs pointers — similarity and difference

Iterators vs pointers — similarity and difference

C++ SELF
Level 40, Lesson 2
Available

1. Introduction

If you are already familiar with T* and with v.begin()/v.end(), a natural question arises: “Why do we have two different ways to iterate elements? Couldn't we keep just one so your brain wouldn't overheat?”. Comparing iterators and pointers is a way to organize your understanding: to see what they have in common, where they differ fundamentally, and why the standard library chose iterators as the primary “language” for containers and algorithms (C++ even has dedicated sections about iterators in the standard documents).

The main goal of this lecture is that, when looking at code, you immediately understand: “aha, here we have direct memory access (pointer), and here access is through a container abstraction (iterator)”. Most importantly: you will stop expecting iterators to behave “exactly like pointers in every situation”. That expectation is one of the most common sources of bugs for beginners.

Reminder: what a pointer is and what it promises

A pointer in C++ is a value that stores the address of an object in memory. It's like a note saying “here's the building and the apartment where the variable lives”. When you write *p, you mean “go to that address and take the object”. When you write p->field, you mean the same, but with convenient field access for a struct/class. And yes, a pointer can be nullptr, meaning “no address”, so pointers are often used to express optionality in an API.

It is important to see the contract here. A pointer by itself does not guarantee that the address is valid, that the object is alive, that the memory belongs to you, or that it is safe to read/write through it. It gives you power (direct memory access), but almost no safety. Hence pointers have the reputation “you can do anything, but one wrong step and you're in trouble”.

A mini example to recall the basic mechanics:

#include <iostream>

int main() {
    int x = 10;
    int* p = &x;

    *p += 5;
    std::cout << x << '\n'; // 15
}

Here p stores the address of x, and *p is access to the same object. No magic: there is a single object.

2. Reminder: what an iterator is and what it promises

An iterator is an object that lets you “walk” over the elements of a container. By feel it really resembles a pointer: you can advance it (++it), compare it (it != end), dereference it (*it). But the key idea is different: an iterator does not have to be a “raw address”. It is a handle or a position marker inside a specific container.

If a pointer says “I know an address in memory”, an iterator says “I know how to reach the current element of the container, according to that container’s rules”. This matters because a container may store elements in ways you do not expect. Even if so far you mainly worked with std::vector, the standard library as a whole is built on the idea that containers can be different, and iterators give a uniform way to traverse them.

In practice, the standard library loves iterators: most algorithms (std::find, std::sort, std::count_if, etc.) take a range as [begin, end) — that is, a pair of iterators.

A small example with std::vector:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    auto it = v.begin();
    std::cout << *it << '\n'; // 10

    ++it;
    std::cout << *it << '\n'; // 20
}

Note: the syntax is “pointer-like”, but the meaning is “position in a container”.

4. Similarity: why an iterator often looks like a pointer

The similarity between iterators and pointers is not accidental: iterators in C++ were conceived as “generalized pointers”. That is, entities that allow you to traverse elements of a data structure and access an element — but not necessarily via a direct address. Therefore their basic operations are very similar: dereference, advance forward, equality/inequality comparison.

A useful analogy: a pointer is “coordinates on a map”, and an iterator is “a navigator that knows the route inside a particular city”. The navigator can also show the “current place” and the “next point”, but you don't have to know how roads are arranged. And if the city suddenly restructures an interchange (the container changes), the navigator may become outdated — here we carefully approach the topic of validity.

Below is a small table to visually reinforce the syntax similarity:

Action Pointer T* p Iterator it
Get element
*p
*it
Access field
p->field
it->field
(if *it is a struct/class)
Move to next
++p
(for arrays/contiguous memory)
++it
Check end
p != end_ptr
it != end_it

One more point that pleasantly surprises beginners: a plain pointer to an array element can serve as an iterator for many algorithms. This is one of the bridges between the “pointer world” and the “STL world”.

For example:

#include <algorithm>
#include <iostream>

int main() {
    int a[5]{3, 1, 4, 1, 5};

    int* begin = a;
    int* end = a + 5;

    auto it = std::find(begin, end, 4);
    std::cout << (it != end) << '\n'; // 1
}

Here int* behaves like an iterator: it can be passed to an algorithm as range boundaries.

5. Differences: an iterator is a position in a container, not an address

Now the most useful part: differences. Similarity is usually obvious, but differences surface as strange errors and questions like “why doesn't this compile, I only wanted it + 2?”.

An iterator is tied to the container and its rules. The container defines what “next element” means, what “end” means, and which operations are allowed. For std::vector an iterator is often implemented as a pointer-like entity (internally it may be an ordinary T*), but that's an implementation detail, not a language guarantee. It is important to get used to the idea: an iterator is not an “address”, but an “access object” that may be more complex than it looks.

Another key difference is the concept of end(). In standard containers end() is the position “past the last element”. It's not an element. It's a stop line. You cannot dereference it. This is very similar to a pointer a + n in an array: it is valid as a boundary marker, but dereferencing it is an error.

Such a range scheme works well:

flowchart LR
    B[begin] --> E1[elem 0] --> E2[elem 1] --> E3[elem 2] --> END[end = past last]

We usually write loops like it != end, because end is not the “last element” but “after the last”.

A typical correct iteration example:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};

    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << ' '; // 1 2 3
    }
    std::cout << '\n';
}

An important thought: if you think “an iterator is a pointer”, you may start expecting that all iterators support, for example, “adding an offset” (it + 5). But that's not always true. Even if you see it works for vector now, don't make that a universal rule. Adopt this rule instead: do only those operations with an iterator that clearly resemble correct traversal: ++, *, comparison with end().

const: const_iterator and “const” pointers

The topic of const may look like “more bureaucracy”, but in practice it honestly highlights the difference between pointers and iterators.

With pointers we have several meanings of const, and that can be confusing. For example, const int* p means “you cannot change the int through p”, whereas int* const p means “the pointer p itself cannot be reassigned, but the int can be changed”.

For iterators the idea is similar but expressed differently: there is a regular iterator (roughly “you can modify elements”) and a const_iterator (roughly “you can only read elements”). When the container itself is const, begin() returns a “reading” iterator, and attempting to modify an element via *it = ... will not compile. This is not “compiler meanness” but built-in protection against accidental modification of data declared const.

Example:

#include <iostream>
#include <vector>

int main() {
    const std::vector<int> v{1, 2, 3};

    auto it = v.begin();          // read iterator
    const int& x = *it;

    std::cout << x << '\n';       // 1
    // *it = 10;                  // compilation error
}

A pointer can also provide such behavior, but an iterator enables it “by default” via container types: the container decides which kind of access is honest.

6. Practice: TaskBook and finding a task

To avoid this topic remaining “theory about some strange objects”, let's continue our small console project TaskBook — a minimal task list based on std::vector<std::string>. We won't do anything architectural or large: just small functions that highlight why an iterator is often more convenient than a pointer in the world of containers.

Assume we already have a list of tasks:

#include <string>
#include <vector>

std::vector<std::string> make_demo_tasks() {
    return {"buy milk", "read cpp", "sleep"};
}

Finding an element: the iterator contract [begin, end)

When you search a task in a vector, the natural STL-style interface is to return an iterator to the found element (or end() if not found). This is a very “native” approach: it immediately works with printing, removal, and modification of the found element.

#include <string>
#include <vector>

std::vector<std::string>::iterator find_task(
    std::vector<std::string>& tasks,
    const std::string& query
) {
    for (auto it = tasks.begin(); it != tasks.end(); ++it) {
        if (*it == query) return it;
    }
    return tasks.end();
}

This returns the “position in the container”. And that's convenient: we are not tied to indices and don't have to mess with std::size_t.

Usage example:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"buy milk", "read cpp", "sleep"};

    auto it = find_task(tasks, "read cpp");
    if (it != tasks.end()) {
        *it += " (30 min)";
    }

    std::cout << tasks[1] << '\n'; // read cpp (30 min)
}

Notice the beauty: we found it — and directly updated the element through the iterator.

Similar search via pointers: it works, but it's a different abstraction level

Now imagine we want to do “the same” but using pointers. A vector has a data() method that returns a T* to the internal array. But here we immediately enter the realm of assumptions: we bind ourselves to the fact that the data is contiguous. For vector this is true, but for containers in general it is not (and that's one of the reasons iterators exist).

Pointer-based search example:

#include <string>
#include <vector>

std::string* find_task_ptr(std::vector<std::string>& tasks, const std::string& q) {
    if (tasks.empty()) return nullptr;

    std::string* p = tasks.data();
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        if (p[i] == q) return &p[i];
    }
    return nullptr;
}

And now usage:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"buy milk", "read cpp", "sleep"};

    if (auto* p = find_task_ptr(tasks, "sleep")) {
        *p = "sleep (8h)";
    }

    std::cout << tasks.back() << '\n'; // sleep (8h)
}

This works, but you can already feel the difference in the “temperature” of the code. The iterator version reads like “I work with the container”. The pointer version reads like “I dug into the container's memory”. Early on this may not be critical, but as the project grows such differences start to affect safety and portability of solutions.

Why iterators are easier to integrate with algorithms

The most practical bonus of iterators is that they plug directly into STL algorithms. And the nicest thing — many algorithms accept a range as iterators, so you can change the container implementation, and the traversal interface remains similar.

For example, let's rewrite find_task using std::find:

#include <algorithm>
#include <string>
#include <vector>

std::vector<std::string>::iterator find_task2(
    std::vector<std::string>& tasks,
    const std::string& query
) {
    return std::find(tasks.begin(), tasks.end(), query);
}

The code became shorter and the meaning clearer: “find in the range”.

7. How to choose: when you need a pointer and when an iterator

If you try to formulate a practical rule without philosophy, it sounds like this: use iterators to traverse containers, use pointers for direct access to an object (especially when the object may be absent). Of course, both tools overlap, and sometimes a pointer acts as an iterator (as with arrays). But in application code a good habit is not to mix abstraction levels without reason.

A pointer is good when you design a function and want to explicitly say: “the parameter may be absent” (nullptr). This is more honest than trying to emulate a nullable reference. Also, a pointer is a natural bridge to low-level APIs and code that works with memory directly (we won't go into that today).

An iterator is good when you think in terms of ranges and want to work with a container without assumptions about its internal layout. The iterator pair [begin, end) is almost the standard “universal socket” for traversal. You pass a range and it is processed uniformly.

A small “cheat-sheet / therapy”:

Situation What to choose more often Why
“Traverse the container” Iterators The container defines traversal rules
“Return the position of a found element” Iterator Easy to compare with end(), easy to erase/modify
“Parameter may be absent” Pointer nullptr is an honest signal
“Working with an array/buffer” Pointers (or begin/end) Pointer is natural for contiguous memory

And yes: if you feel the urge “to treat an iterator like an address because it's easier” — that's often a signal that you're trying to solve the problem at the wrong level. Stop for a second and ask yourself: “am I thinking about the container or about memory?”.

8. Common mistakes when comparing iterators and pointers

Mistake #1: assuming an iterator is always “just a pointer”.
On std::vector iterators often behave very similarly to pointers, and that makes you relax. But an iterator is an abstraction, not a guarantee of contiguous memory. If you get used to doing everything with an iterator that you do with a T*, you will soon end up in situations where “it seems logical” but “for some reason it doesn't compile”. The right habit is to use a minimal operation set: *, ++, comparison with end().

Mistake #2: dereferencing end() or “past the last”.
end() is not an element. It's the range boundary. Dereferencing *end() is a logic error. Pointers have the same trap: a + n can be held as a marker, but you cannot *(a + n). If you want “the last element”, usually use --end() (but carefully) or container methods like back() (if the container is not empty).

Mistake #3: storing an iterator/pointer to an element and forgetting that the container might change.
Even without a detailed invalidation table (you will see that later), remember the general principle: if a container changes its internal layout (for example it grows and “moves” to another location), old “bindings” to elements can become invalid. Beginners often take an it to an element, then call push_back somewhere, and later use it again and wonder about strange behavior. Discipline helps here: either don't keep such bindings for long, or strictly control container modifications.

Mistake #4: confusing “a copy of the value” with “a reference to the element” when dereferencing.
In expressions like auto x = *it you get a copy of the element, whereas auto& x = *it — a reference to the element. In the first case changes to x do not modify the container; in the second they do. The same idea applies with pointers (int x = *p vs int& x = *p). This mistake is especially annoying: the program compiles and runs, but does “the wrong thing”, and you spend half an hour arguing with the monitor.

Mistake #5: using pointers where you mean “traverse a range”, and vice versa.
Sometimes a student writes a function “traverse the vector” and accepts T* and size, because “they already know pointers”. This works, but you lose the container's meaning and increase the chance of boundary errors. And sometimes the opposite happens: trying to express optionality with an iterator, while a pointer (T*/nullptr) would be more honest. A good interface usually reads like a proposal: if you want traversal — take iterators; if something may be absent — take a pointer.

1
Task
C++ SELF, level 40, lesson 2
Locked
Buffer Sum
Buffer Sum
1
Task
C++ SELF, level 40, lesson 2
Locked
Even frames
Even frames
1
Task
C++ SELF, level 40, lesson 2
Locked
Log label
Log label
1
Task
C++ SELF, level 40, lesson 2
Locked
Two searches
Two searches
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION