The for Loop, Taken Apart
A for loop packs three separate jobs into one header, separated by semicolons: an initialiser that runs once before anything else, a condition checked before every iteration, and an update that runs after every iteration. Knowing the exact order those fire in removes most confusion about what a loop actually did.
The sequence is: run the initialiser; test the condition — if false, stop immediately; run the body; run the update; test the condition again; repeat. Notice that the condition is checked before the first pass, which means a for loop can legitimately execute its body zero times. That is usually the behaviour you want when iterating over a list that might be empty.
The variable declared in the initialiser lives only inside the loop. That is a genuine feature: it cannot be read after the loop by accident, and you can reuse the name i in the next loop without a thought. If you actually need the counter's final value afterwards, declare it before the loop instead.
The mistake that costs marks is the off-by-one. For a container of size n, the valid indices run from 0 to n - 1. So i < n is correct and i <= n reads one element past the end — undefined behaviour that often prints a plausible-looking garbage number rather than crashing, which is precisely what makes it hard to spot. When you are unsure, count the iterations by hand for a container of size 1 and a container of size 0; those two cases catch nearly every boundary error.
#include <iostream>
#include <vector>
int main() {
// init ; condition ; update
for (int i = 0; i < 5; ++i) {
std::cout << i << ' '; // 0 1 2 3 4
}
std::cout << '\n';
// std::cout << i; // error: i does not exist out here
std::vector<int> marks = {72, 65, 91};
for (int i = 0; i < static_cast<int>(marks.size()); ++i) {
std::cout << "Student " << i + 1 << ": " << marks[i] << '\n';
}
// i <= size() would read marks[3], which does not exist
// for (int i = 0; i <= marks.size(); ++i) { ... } // BUG
// Counting down
for (int i = static_cast<int>(marks.size()) - 1; i >= 0; --i) {
std::cout << marks[i] << ' '; // 91 65 72
}
std::cout << '\n';
} - Use
++irather thani++in the update clause as a default habit. For anintthe compiled result is identical, but for iterators and other class typesi++has to build and return a copy of the old value that you then throw away. Getting used to the cheap form costs nothing.
while and do-while
Use a for loop when you know in advance how many times you are going round — over a container, over a range of numbers. Use a while loop when you do not: keep reading until the input ends, keep searching until you find the answer, keep halving until the interval is small enough. Both are equally powerful, so this is purely about which one communicates your intent.
while (condition) checks first and may run zero times. do { ... } while (condition); checks after the body, so it always runs at least once. That difference is exactly what you want for input validation: you have to ask the user before you can judge their answer, so the ask belongs in the body and the test belongs at the bottom. Note the semicolon after the closing while of a do-while — it is required, and leaving it out produces a confusing error.
The characteristic failure of a while loop is the infinite one, and it has exactly one cause: nothing inside the body changes the value the condition depends on. In a for loop the update clause sits in the header where it is hard to forget. In a while loop it is your responsibility, buried somewhere in the body, and it is easy to lose during an edit — particularly if a continue jumps over it.
If you write a loop that is genuinely meant to run until something inside it stops it, say so with while (true) and a clear break. It is honest, it is recognisable, and it is much easier to review than while (running) where running is set in three different places.
#include <iostream>
#include <limits>
int main() {
// while: unknown number of iterations
int n = 1;
int steps = 0;
while (n < 1000) {
n *= 2; // this line is what stops the loop
++steps;
}
std::cout << steps << " doublings\n"; // 10
// do-while: must run at least once (ask, then judge)
int age = 0;
do {
std::cout << "Enter your age (1-120): ";
std::cin >> age;
if (!std::cin) { // they typed letters
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
age = 0;
}
} while (age < 1 || age > 120); // <- semicolon required
std::cout << "Age accepted: " << age << '\n';
// An honest infinite loop with a clear exit
int menu = 0;
while (true) {
std::cout << "1) list 2) add 0) quit: ";
std::cin >> menu;
if (menu == 0) break;
std::cout << "you chose " << menu << '\n';
}
} - A subtle infinite-loop cause:
continueinside awhilejumps straight back to the condition, skipping anything below it in the body — including your counter update. In aforloopcontinuestill runs the update clause, because the update lives in the header. This asymmetry has cost many people an afternoon.
Range-Based for: The One to Reach For
Since C++11 there is a loop that says "do this to every element" without any index arithmetic at all: for (element : container). It works on arrays, on every standard container, and on anything that provides begin and end. When you do not need the index, this is the loop to write — there is no counter to get wrong, no <= versus < decision, and no way to run off the end.
The one thing you must get right is how you take the element, and the choice has real consequences. for (auto x : v) copies each element into x. For a vector of int that is free; for a vector of std::string or of large objects it means a fresh allocation and copy on every single iteration, for no benefit. for (const auto& x : v) binds a read-only reference to each element and copies nothing — this should be your default when you are only reading. for (auto& x : v) binds a modifiable reference, which is what you want when the loop is meant to change the container's contents.
For maps, C++17 structured bindings make this genuinely pleasant: for (const auto& [name, marks] : record) unpacks each key-value pair into two named variables, instead of you writing pair.first and pair.second and trying to remember which is which.
The rule that keeps you safe: do not add to or remove from a container while you are iterating over it with a range-based for. A push_back can force the vector to allocate a bigger buffer and move everything to it, at which point the loop's internal pointers refer to freed memory and the behaviour is undefined. If you need to build a modified version, collect into a second container, or use the erase-remove idiom covered in the STL algorithms lesson.
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names = {"Ananya", "Rahul", "Meera"};
for (const auto& n : names) { // read-only, no copies
std::cout << n << ' ';
}
std::cout << '\n';
for (auto& n : names) { // modify in place
n += " (present)";
}
// for (auto n : names) { } // copies a whole string each time
std::map<std::string, int> marks = {{"Ananya", 91}, {"Rahul", 65}};
for (const auto& [name, score] : marks) { // C++17 structured bindings
std::cout << name << ": " << score << '\n';
}
// NEVER do this — growing the vector invalidates the loop
// for (const auto& n : names) {
// names.push_back(n); // undefined behaviour
// }
std::cout << names.size() << '\n'; // 3
} - Reach for a plain indexed
foronly when you genuinely need the index — printing a numbered list, comparingv[i]withv[i + 1], or walking two containers in step. Otherwise the range-based form is shorter, safer and just as fast.
break, continue, and the Unsigned Trap
break leaves the loop entirely and carries on after it. continue abandons the current iteration and goes on to the next one. Both are useful for expressing "I am done here" without contorting the loop condition, and both should be used sparingly enough that a reader can still see the shape of the loop.
The critical detail about break in nested loops: it exits only the innermost loop it is inside. If you are searching a two-dimensional grid and find your answer, one break escapes the inner loop and the outer loop cheerfully starts the next row. The cleanest fix by far is to move the search into its own function and return from it — a return exits everything, and the function ends up with a name that says what it does. The alternative, a boolean flag tested by the outer loop, works but adds a variable and a condition to keep in sync.
Now the trap that catches almost everyone at least once. v.size() returns an unsigned type, std::size_t. Two consequences follow. First, comparing it against a signed int produces a warning under -Wall -Wextra, because the int gets converted to unsigned and any negative value becomes enormous. Second, and worse, v.size() - 1 on an empty vector does not give −1; unsigned arithmetic wraps, so it gives the largest possible size_t. A loop written as for (size_t i = 0; i < v.size() - 1; ++i) is correct for every non-empty vector and reads billions of elements past the end for an empty one.
Two safe habits fix this permanently. Write the comparison as i + 1 < v.size() instead of i < v.size() - 1, since addition on the left never wraps into a huge value. And when counting downwards, use a signed int index — a size_t is never negative, so for (size_t i = v.size() - 1; i >= 0; --i) is an infinite loop by construction.
#include <iostream>
#include <vector>
// Searching a grid: return beats break, and exits both loops
bool contains(const std::vector<std::vector<int>>& grid, int target) {
for (const auto& row : grid) {
for (int value : row) {
if (value == target) return true; // leaves everything
}
}
return false;
}
int main() {
for (int i = 0; i < 10; ++i) {
if (i % 2 == 0) continue; // skip evens
if (i > 5) break; // stop once past 5
std::cout << i << ' '; // 1 3 5
}
std::cout << '\n';
std::vector<int> v; // empty!
// BUG: v.size() - 1 wraps to a huge number when v is empty
// for (std::size_t i = 0; i < v.size() - 1; ++i) { std::cout << v[i]; }
// Safe: addition on the left cannot wrap
for (std::size_t i = 0; i + 1 < v.size(); ++i) {
std::cout << v[i] << " then " << v[i + 1] << '\n';
}
// Counting down needs a SIGNED index: size_t is never < 0
for (int i = static_cast<int>(v.size()) - 1; i >= 0; --i) {
std::cout << v[i] << ' ';
}
std::cout << contains({{1, 2}, {3, 4}}, 3) << '\n'; // 1
} - If the compiler warns
comparison of integer expressions of different signedness, do not silence it with a cast and move on. Read the loop and decide whether the size could ever be zero. That warning is pointing at a real class of bug, not a style preference.
