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.
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 (
++iis 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
whileloop to an equivalentforloop (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
whileor a boundedforwith separate input variable.
5) Common For-Loop Errors (and fixes)
- Off-by-one: using
<=instead of<when indexing. - Signed/unsigned mismatch: compare
int iwithsize_t size→ prefersize_tfor 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
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::rangespipeline 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
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_tfor 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
| Part | When it runs | Question it must answer |
|---|---|---|
| Initialisation | Once, before the first condition test | What is the first candidate state? |
| Condition | Before every possible execution of the body | Is the current candidate still inside the required range? |
| Body | Only when the condition is true | What work belongs to this candidate? |
| Update | After a completed body and before the next test | How 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 value | sum before the body | Body effect | State after update |
|---|---|---|---|
| 2, true | 0 | Add 2, giving 2 | value becomes 4 |
| 4, true | 2 | Add 4, giving 6 | value becomes 6 |
| 6, true | 6 | Add 6, giving 12 | value becomes 8 |
| 8, true | 12 | Add 8, giving 20 | value becomes 10 |
| 10, true | 20 | Add 10, giving 30 | value becomes 12 |
| 12, false | 30 | Body is not executed | Update 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 loop | Observed sequence | Judgement |
|---|---|---|
value < 10 | 2, 4, 6, 8 | Terminates, but omits a required endpoint. |
value <= 12 | 2, 4, 6, 8, 10, 12 | Terminates, but admits a value outside the requirement. |
value += 4 | 2, 6, 10 | Stays inside the bound, but no longer visits every required even value. |
No update to value | 2 repeatedly | The 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 test | sum before the body | sum after the body | Next value |
|---|---|---|---|
2 <= 14, true | 0 | 2 | 4 |
4 <= 14, true | 2 | 6 | 6 |
6 <= 14, true | 6 | 12 | 8 |
8 <= 14, true | 12 | 20 | 10 |
10 <= 14, true | 20 | 30 | 12 |
12 <= 14, true | 30 | 42 | 14 |
14 <= 14, true | 42 | 56 | 16 |
16 <= 14, false | 56 | Body not entered | No 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.
}
| Length | Indices admitted to the body | First failed test | Exit claim |
|---|---|---|---|
| 0 | None | 0 < 0 | An empty sequence has no valid index. |
| 1 | 0 | 1 < 1 | The only valid index was visited once. |
| 5 | 0, 1, 2, 3, 4 | 5 < 5 | Every 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.