Arrays and loops

In the introductory lesson for this chapter, we explored the scalability problems that arise when managing many related variables. Arrays provide an elegant solution, but their true power emerges when combined with loops.

The scalability challenge revisited

Imagine calculating the average test score for a student across five exams:

#include <iostream>

int main()
{
    int exam1{ 85 };
    int exam2{ 92 };
    int exam3{ 78 };
    int exam4{ 90 };
    int exam5{ 88 };

    int avg{ (exam1 + exam2 + exam3 + exam4 + exam5) / 5 };

    std::cout << "Average score: " << avg << '\n';

    return 0;
}

This approach is tedious, error-prone, and doesn't scale. What if we track scores across a dozen exams? Or an entire semester? Using a std::vector helps, but we still face challenges:

#include <iostream>
#include <vector>

int main()
{
    std::vector testScores{ 85, 92, 78, 90, 88 };
    std::size_t count{ testScores.size() };

    int avg{ (testScores[0] + testScores[1] + testScores[2] + testScores[3] +
              testScores[4])
            / static_cast<int>(count) };

    std::cout << "Average score: " << avg << '\n';

    return 0;
}

Better, but still fragile. Adding or removing scores requires modifying multiple lines. We need a way to access each element automatically.

Combining arrays with loops

Array indices form a sequential pattern: 0, 1, 2, 3, 4. We can use a loop variable to generate this sequence:

#include <iostream>
#include <vector>

int main()
{
    std::vector testScores{ 85, 92, 78, 90, 88 };
    std::size_t count{ testScores.size() };

    int total{ 0 };
    for (std::size_t idx{ 0 }; idx < count; ++idx)
        total += testScores[idx];

    int avg{ total / static_cast<int>(count) };

    std::cout << "Average score: " << avg << '\n';

    return 0;
}

The loop iterates from 0 to count - 1, accessing each element sequentially. When idx reaches count, the condition idx < count becomes false, terminating the loop.

This solution is maintainable. The loop adapts automatically to the vector's length—add or remove elements, and the code still works correctly.

Traversal (or iteration) refers to accessing each container element in sequence.

Templates, arrays, and loops unlock true scalability

Arrays store multiple related values without individual naming. Loops traverse arrays without explicit enumeration. Templates parameterize element types.

Together, these features enable code that works regardless of element type or container size:

#include <iostream>
#include <vector>

template <typename T>
T calculateAverage(const std::vector<T>& data)
{
    std::size_t count{ data.size() };

    T sum{ 0 };
    for (std::size_t idx{ 0 }; idx < count; ++idx)
        sum += data[idx];

    return sum / static_cast<int>(count);
}

int main()
{
    std::vector testScores{ 85, 92, 78, 90, 88 };
    std::cout << "Average score (5 exams): " << calculateAverage(testScores) << '\n';

    std::vector timings{ 1.25, 1.18, 1.32, 1.15 };
    std::cout << "Average timing (4 runs): "
              << calculateAverage(timings) << '\n';

    return 0;
}

Output:

Average score (5 exams): 86
Average timing (4 runs): 1.225

Our calculateAverage() function template works with any element type and any vector length!

Common array traversal patterns

We typically traverse containers for these purposes:

  1. Calculate aggregate values: sum, average, min, max
  2. Search for elements: find specific values, count matches, locate extremes
  3. Transform elements: output each element, apply operations to all elements
  4. Reorder elements: sort, shuffle, reverse

The first three use a single loop. Reordering typically requires nested loops or specialized algorithms from the standard library.

Avoiding off-by-one errors

When traversing with indices, ensure your loop executes the correct number of times. Off-by-one errors—where the loop runs once too many or too few times—are common pitfalls.

Typically, start at index 0 and continue while idx < length:

for (std::size_t idx{ 0 }; idx < testScores.size(); ++idx)
    // Process testScores[idx]

A common mistake is using idx <= testScores.size():

for (std::size_t idx{ 0 }; idx <= testScores.size(); ++idx)  // Wrong!
    // This executes when idx equals size, causing out-of-bounds access