C++ Programming
Lesson 10 of 24

Lesson 10 · 24 lesson course

10. The For Loop

Repeating code with conditions a known number of times

Programming glossary 44 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

Abstract measured ring of repeated steps with one highlighted iteration position.

 

Lesson: For Loops, Flow Control & Memory Locality (C++)

Flow control is how programmers alter the order of execution of statements. Under the hood, the compiler emits jumps in machine code so execution can repeat or skip instructions. While CPUs like to run instructions serially (for cache friendliness), structured control flow (loops, conditionals) is essential for real programs.


1) A Little Bit About Memory

Processes see a virtual address space managed by the OS + MMU and mapped onto real hardware memory. Data might live:

  • In the CPU caches (very fast, very small)
  • In main memory (RAM, slower, large)
  • On disk/swap (very slow, fallback)

The system tries to keep the data you’ll need near the CPU. If code jumps around unpredictably, you can suffer cache misses and even page faults (RAM pages swapped in/out), which stall the CPU. Compilers often improve layout and simplify conditional paths, but your loop structure still matters for locality.

Programming Insight (AI) — Make Loops Cache-Friendly
  • Paste a loop; ask AI if iteration order matches memory layout (e.g., row-major vs column-major).
  • Have AI suggest loop fusion/splitting or reserve()/shrink_to_fit() calls to reduce reallocations and jumps.

2) The for Loop Shape

A canonical for loop has three parts: initialisation; condition; iteration. The condition is checked at the start of each iteration.

for (int i = 0; i < n; ++i) {     // init ; condition ; step
    // loop body
}
  • The control variable (i) is usually declared in the loop and lives only inside it (good scoping).
  • Iterations should be known (or upper-bounded) at runtime; compilers may optimise if the bound is known at compile time.
  • Follow a simple, monotonic sequence (++i is idiomatic).
Programming Insight (AI) — Specify Loop Contracts
  • Ask AI to state your loop’s invariant (“what stays true each iteration”) and postcondition (“what’s true when it ends”).
  • Have AI convert a while loop to an equivalent for loop (or vice versa) and explain the differences.

3) Pick the Right Loop

  • for: fixed/countable iterations (indexing arrays/vectors).
  • range-based for: iterate elements directly; simplest and safest.
  • while: sentinel-controlled input or loops with no clear count.
#include <vector>
#include <iostream>
int main() {
    std::vector<int> v{1,2,3};

    // index form (need indices)
    for (std::size_t i = 0, n = v.size(); i < n; ++i) {
        v[i] *= 2;
    }

    // range-based form (prefer when index not needed)
    for (int& x : v) {
        x *= 2;
    }
}
Programming Insight (AI) — Convert to Safer Forms
  • Ask AI to turn index loops into range-based loops where safe, and to keep index form only when needed.
  • Have AI suggest using algorithms (std::for_each, std::transform) when intent is “apply function to all”.

4) The Classic Pitfall — Modifying the Control Variable

Never change the loop’s control variable from inside the body for unrelated reasons (especially via input). It risks infinite loops or skipped iterations.

int x = 100;

for (int y = 0; y < x; ++y) {
    std::cin >> y;                 // ❌ BAD: mutates control variable
    std::cout << x + y << '\n';
}

Why it’s bad: the loop condition depends on y, and you’re changing y in a non-monotonic way based on user input — this can cause non-termination or surprising behaviour.

Better patterns:

// A) Separate the input from the control variable
int x = 100;
for (int y = 0; y < x; ++y) {
    int input{};
    std::cin >> input;
    std::cout << x + input << '\n';
}

// B) If the user controls loop length, use a while with a clear sentinel/condition
int input{};
while (std::cin >> input && input != 0) {  // stop on 0 (sentinel)
    std::cout << "Result: " << (100 + input) << '\n';
}
Programming Insight (AI) — Prevent “Never-Ending” Loops
  • Paste the loop; ask AI to identify which variable controls termination and whether it always progresses.
  • Have AI propose a sentinel-based while or a bounded for with separate input variable.

5) Common For-Loop Errors (and fixes)

  • Off-by-one: using <= instead of < when indexing.
  • Signed/unsigned mismatch: compare int i with size_t size → prefer size_t for indices.
  • Changing the bound inside the loop (e.g., pushing to a vector you’re iterating by index without planning for growth).
  • Recomputing size in the condition when it can change; capture it first if appropriate.
std::vector<int> v = {1,2,3};
// ❌ off-by-one
// for (size_t i = 0; i <= v.size() - 1; ++i) { ... }

for (size_t i = 0, n = v.size(); i < n; ++i) { // ✅ capture size
    // safe indexing
}
Programming Insight (AI) — Lint Your Loops
  • Ask AI to scan for off-by-one risks, signed/unsigned compares, and bounds that change mid-loop.
  • Have AI propose unit tests (empty, single-element, large sizes) to catch edge cases.

6) Performance Notes (Beginners’ Edition)

  • Prefer contiguous data access (arrays, std::vector) and forward iteration.
  • Keep the body small and predictable; avoid unnecessary branches inside hot loops.
  • If the iteration count is a compile-time constant (constexpr N), compilers may unroll or vectorise.
constexpr int N = 1024;
int a[N];
for (int i = 0; i < N; ++i) { a[i] = i; } // compiler can unroll/vectorise
Programming Insight (AI) — Microbench & Interpret
  • Have AI generate a small benchmark (Google Benchmark) to compare two loop variants.
  • Ask AI to explain profiler output (hot lines, cache misses) in plain English.

7) Worked Example — Sum of Even Numbers

#include <vector>
#include <iostream>

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

    // Counted for
    int sum = 0;
    for (size_t i = 0, n = v.size(); i < n; ++i) {
        if (v[i] % 2 == 0) sum += v[i];
    }
    std::cout << "sum=" << sum << '\n';

    // Range-based for (clearer intent)
    int sum2 = 0;
    for (int x : v) {
        if ((x & 1) == 0) sum2 += x;
    }
    std::cout << "sum2=" << sum2 << '\n';
}
Programming Insight (AI) — From Loops to Algorithms
  • Ask AI to rewrite as std::accumulate/std::ranges pipeline and to comment the trade-offs.

8) Mini Exercise — Fix the Loop

Problem: The user should enter numbers; print 100 + input until the user enters 0. The current code is wrong:

int x = 100;
for (int y = 0; y < x; ++y) {
    std::cin >> y;                   // ❌
    std::cout << x + y << '\n';
}

Target (one possible fix):

int x = 100;
for (int input = 0; std::cin >> input && input != 0; ) {
    std::cout << x + input << '\n';
}
Programming Insight (AI) — Repair Recipe
  • Ask AI to identify the control variable and propose a loop where the control and input are separate.
  • Have AI generate tests: empty input, single value, long sequence, invalid input handling.

Summary Checklist

  • Use for when iteration count is known; range-based for when you just need elements; while for sentinel input.
  • Keep the control variable monotonic and separate from user inputs or side effects.
  • Prefer < to <= for index bounds; use size_t for sizes/indices.
  • Maximise locality: contiguous data, simple branches, predictable strides.
  • Let the compiler optimise; write clear loops first, then measure.

Advanced perspective: counted iteration and invariants

A for loop describes a controlled sequence of states

A loop that prints the expected answer once may still contain the wrong bound, the wrong update or an accidental dependence on the data used for that run. The operative test is whether its initial state, continuation condition, body and update jointly describe the required sequence. Each part has a job. If you cannot account for the state at the boundary between two iterations, the final answer is weaker evidence than it appears.

The three expressions in a conventional for header gather the control policy into one visible place. That is why the form is useful for counted work. The syntax itself does not promise a known number of iterations, or even termination. Those properties must follow from the actual values and transitions.

Execution order is the first thing to get right

PartWhen it runsQuestion it must answer
InitialisationOnce, before the first condition testWhat is the first candidate state?
ConditionBefore every possible execution of the bodyIs the current candidate still inside the required range?
BodyOnly when the condition is trueWhat work belongs to this candidate?
UpdateAfter a completed body and before the next testHow does the control state move towards the next candidate and, eventually, the end?

The order is therefore: initialise, test, execute the body, update, test again. The initialisation does not repeat. The update does not happen before the first body. When the condition is false, the body and update are skipped and execution continues after the loop. A continue statement inside a for loop also leads to the update before the next condition test; it does not jump directly to the condition.

When the control variable is declared in the initialisation, it is available to the condition, the body and the update. It is not available after the for statement has finished. That scope is useful evidence: the name exists for the policy it controls, then disappears.

Trace control state and useful state separately

#include <iostream>

int main() {
    int sum{0};

    for (int value{2}; value <= 10; value += 2) {
        sum += value;
    }

    std::cout << "Sum: " << sum << '\n';
}

Here, value controls the sequence and sum records the work already completed. Confusing those roles is dangerous. The loop must advance value even though the calculation of interest is stored in sum.

Condition test with valuesum before the bodyBody effectState after update
2, true0Add 2, giving 2value becomes 4
4, true2Add 4, giving 6value becomes 6
6, true6Add 6, giving 12value becomes 8
8, true12Add 8, giving 20value becomes 10
10, true20Add 10, giving 30value becomes 12
12, false30Body is not executedUpdate is not executed

The output is Sum: 30 followed by a newline. More importantly, the trace explains why. Before each condition test, sum is the total of the required even values that are less than value. This statement is the loop invariant.

It is true initially: no required value is less than 2, and the sum is zero. If it is true when the condition admits the current value, the body adds that value and the update moves to the next even candidate, so the statement is true again. At exit, value is 12 and the condition is false. The invariant then tells us that all required even values through 10 have been included. Initial truth, preservation and a useful exit condition turn the trace into a correctness argument.

A bound is correct only in relation to the intended range

The use of <= in this program is deliberate because 10 belongs to the required arithmetic range. Replacing it with < would omit 10 and produce 20. This is not an argument that inclusive bounds are generally safer. It shows that a comparison has no meaning in isolation from the range it represents.

Indices usually describe a different range. If a sequence contains count elements, its valid indices are normally zero through count - 1. The half-open form 0 <= index && index < count states that contract directly. It also handles an empty sequence: when count is zero, the first test fails and the body does not run.

Change to the worked loopObserved sequenceJudgement
value < 102, 4, 6, 8Terminates, but omits a required endpoint.
value <= 122, 4, 6, 8, 10, 12Terminates, but admits a value outside the requirement.
value += 42, 6, 10Stays inside the bound, but no longer visits every required even value.
No update to value2 repeatedlyThe condition does not progress towards false, so the intended termination argument fails.

This is why counting the number of body executions is only one check. A loop can execute five times and still visit the wrong five values. The actual sequence is the evidence.

Separate the invariant, progress and exit claim

Three questions expose most defects in a counted loop. What must be true whenever the condition is tested? Which state change moves the loop towards termination? What does the false condition establish when the loop exits? An invariant without progress can describe an endless loop. Progress without the right invariant can terminate after doing the wrong work. A false condition that says nothing useful about completed work cannot justify the result on its own.

Modifying the control variable inside the body deserves special suspicion because it creates a second update policy away from the header. It is not forbidden by C++, and there are specialised loops where it is intentional. However, if the body reads user input into the control variable, the next candidate and the termination argument now depend on arbitrary input. A separate input variable keeps the counted policy inspectable.

A range-based for loop is often clearer when the requirement is simply to process every element and the position has no separate meaning. A conventional counted loop remains appropriate when the index is part of the result, a subrange is required or the step itself matters. Choose the form that makes the required state visible. Familiar syntax is not the criterion.

Performance follows correctness, not the other way round

Iteration order can affect performance because data representation and access pattern influence locality. That claim does not rescue an unsafe bound, and it does not prove that a short benchmark predicts the complete application. Establish the visited range and state transitions first. If performance is material, measure the real workload and interpret the result in the context of its data size, layout, compiler settings and machine.

Programming Insight (AI): demand the states, not a reassuring verdict

When an AI system proposes or reviews a loop, ask for the initial state, the first three condition tests, the final successful test and the first failed test. Require it to name the control variable, state the invariant and identify the progress step. Then compare those claims with the source. A conventional-looking header can still visit the wrong range, and a confident claim that the loop is safe is not a trace.

Prove a small change before you run it

Change the upper endpoint of the worked requirement from 10 to 14. Before editing the condition, predict every admitted value, every value of sum after the body and the first failed condition test.

Then test a half-open index loop with sequence lengths zero, one and five. For each length, write the candidate indices before running the program. If the body executes for an index equal to the length, the boundary is wrong. Stop at the first failed test, inspect the state and explain the exit claim. That discipline scales beyond this example because it tests the loop's argument, not your confidence in its appearance.

Reveal answer

The final sum should be 56, but reaching 56 is not the whole task: your trace must show why 2, 4, 6, 8, 10, 12 and 14 are each included once.

The new endpoint belongs to the required range, so the changed loop condition is value <= 14:

int sum{0};

for (int value{2}; value <= 14; value += 2) {
    sum += value;
}
Condition testsum before the bodysum after the bodyNext value
2 <= 14, true024
4 <= 14, true266
6 <= 14, true6128
8 <= 14, true122010
10 <= 14, true203012
12 <= 14, true304214
14 <= 14, true425616
16 <= 14, false56Body not enteredNo update

Before each test, sum is the total of the required even values from 2 up to, but not including, value. The body preserves that statement by adding the current value, and the update advances to the next even candidate. The failed test at 16 establishes that every required value through 14 has been admitted once. The value 56 is therefore explained by the state transitions rather than accepted because it appeared on one run.

A half-open index loop has a different boundary:

for (std::size_t index{0}; index < length; ++index) {
    // Use the element at index.
}
LengthIndices admitted to the bodyFirst failed testExit claim
0None0 < 0An empty sequence has no valid index.
101 < 1The only valid index was visited once.
50, 1, 2, 3, 45 < 5Every valid index was visited once.

In every case, the candidate equal to length is tested and rejected before the body. That is the evidence that the loop does not step outside the sequence.