C++ 数组与循环:高效处理集合数据的艺术

在本章的导入课(容器与数组简介)中,我们介绍了当存在大量彼此关联的独立变量时所带来的可扩展性难题。本课将重新审视这一问题,并讨论如何利用数组优雅地解决此类问题。

变量可扩展性问题的再审视

在本章的导入课(容器与数组简介)中,我们介绍了当存在大量彼此关联的独立变量时所带来的可扩展性难题。本课将重新审视这一问题,并讨论如何利用数组优雅地解决此类问题。

变量可扩展性问题的再审视

假设我们要计算一个班级学生的平均测验成绩。为使示例简洁,假设班级仅有 5 名学生。

若使用独立变量,代码可能如下:

#include <iostream>

int main()
{
    // 分配 5 个整型变量(各自名称不同)
    int testScore1{ 84 };
    int testScore2{ 92 };
    int testScore3{ 76 };
    int testScore4{ 81 };
    int testScore5{ 56 };

    int average{ (testScore1 + testScore2 + testScore3 + testScore4 + testScore5) / 5 };

    std::cout << "The class average is: " << average << '\n';

    return 0;
}

变量众多,输入繁琐。若班级有 30 人甚至 600 人,工作量将剧增。此外,一旦新增成绩,就必须再声明、初始化一个变量,并把它加入平均值计算。你是否记得同步更新除数?若忘记,便会产生语义错误。任何需要修改现有代码的时刻,都可能引入新的缺陷。

至此你已知晓,当拥有一批彼此相关的变量时,应使用数组。下面用 std::vector 替换独立变量:

#include <iostream>
#include <vector>

int main()
{
    std::vector testScore{ 84, 92, 76, 81, 56 };
    std::size_t length{ testScore.size() };

    int average{ (testScore[0] + testScore[1] + testScore[2] + testScore[3] + testScore[4])
        / static_cast<int>(length) };

    std::cout << "The class average is: " << average << '\n';

    return 0;
}

这已有所改善——变量数量大幅减少,且平均值计算所用的除数直接由数组长度决定。

然而,平均值计算依旧存在问题:我们仍需手动列出每个元素。由于显式列出,计算逻辑只能适用于元素数量完全匹配的情形。若要处理不同长度的数组,就必须为每种长度重写平均值计算。

我们真正需要的是:无需逐一枚举即可依次访问每个数组元素的方法。

数组与循环

在先前课程中,我们指出数组下标不必是常量表达式——它们可以是运行时表达式。这意味着可用变量值作为下标。

同时注意,上例平均值计算中的下标呈升序:0, 1, 2, 3, 4。因此,若能令某变量依次取值 0, 1, 2, 3, 4,即可用该变量代替字面量作为下标。而实现这一点我们早已掌握——使用 for 循环。

相关内容
for 循环见课程 8.10 —— for 语句。

下面用 for 循环改写示例,以循环变量当下标:

#include <iostream>
#include <vector>

int main()
{
    std::vector testScore{ 84, 92, 76, 81, 56 };
    std::size_t length{ testScore.size() };

    int average{ 0 };
    for (std::size_t index{ 0 }; index < length; ++index) // index 从 0 到 length-1
        average += testScore[index];                      // 累加下标为 index 的元素
    average /= static_cast<int>(length);                  // 求平均

    std::cout << "The class average is: " << average << '\n';

    return 0;
}

逻辑简明:index 初值为 0,累加 testScore[0] 后自增到 1,再加 testScore[1],如此反复,直至 index 变为 5 时条件 index < length 为假,循环结束。

至此,循环已将 testScore[0..4] 的值累加至 average,随后除以数组长度得到平均。

该方案在可维护性上堪称理想:循环次数由数组长度决定,循环变量完成所有下标工作,无需人工列举元素。

增删成绩时,只需调整初始化列表元素个数,其余代码无需改动!

按某种次序访问容器内每个元素称为遍历(traversal),也常称迭代(iteration),即“在容器上迭代”或“遍历容器”。

作者注
由于容器类使用 size_t 表示长度和下标,本课亦遵循此惯例。带符号长度与下标的讨论见后续课程 16.7 —— 数组、循环与符号难题解决。

模板、数组与循环带来的可扩展性

  • 数组:无需为每个元素命名即可存储多个对象。
  • 循环:无需显式列举即可遍历数组。
  • 模板:可对元素类型进行参数化。

三者结合,使我们能编写出不受元素类型及数量限制、可作用于任意容器的代码。

为进一步说明,下面把平均值计算重构为函数模板:

#include <iostream>
#include <vector>

// 计算 std::vector 中元素平均值的函数模板
template <typename T>
T calculateAverage(const std::vector<T>& arr)
{
    std::size_t length{ arr.size() };

    T average{ 0 };                                      // 若数组元素类型为 T,平均值也应是 T
    for (std::size_t index{ 0 }; index < length; ++index) // 遍历所有元素
        average += arr[index];                            // 累加
    average /= static_cast<int>(length);                  // 除以元素个数(整型)

    return average;
}

int main()
{
    std::vector class1{ 84, 92, 76, 81, 56 };
    std::cout << "The class 1 average is: " << calculateAverage(class1) << '\n'; // 5 个 int 的平均

    std::vector class2{ 93.2, 88.6, 64.2, 81.0 };
    std::cout << "The class 2 average is: " << calculateAverage(class2) << '\n'; // 4 个 double 的平均

    return 0;
}

输出:

The class 1 average is: 77
The class 2 average is: 81.75

上例中,calculateAverage() 可接受任意元素类型、任意长度的 std::vector 并返回平均值。main() 演示了对 5 个 int 与 4 个 double 的调用均能正确工作。

只要类型 T 支持函数体内用到的运算符(operator+=(T)operator/=(int)),calculateAverage() 即可使用;若 T 不支持,编译器会在实例化时报错。

你可能疑问为何把 length 强转为 int 而非 T。求平均值时,总和除以元素个数,而元素个数是整型,因此语义上应除以整型。

数组与循环可做的事

已知可用循环遍历容器,下面列出最常见的四类需求:

  1. 基于元素计算新值(如求平均、求和)。
  2. 查找元素(如是否存在、计数、找最大值)。
  3. 对每个元素操作(如输出、全部乘 2)。
  4. 重排元素(如升序排序)。

前三者相对直接,可用单循环实现。重排则需嵌套循环,通常最好改用标准库算法,后续章节详述。

数组与差一错误

用下标遍历容器时,务必确保循环次数正确。差一错误(循环多一次或少一次)极易发生。

通常遍历时,下标从 0 开始,终止条件为 index < length

新手常误用 index <= length 作为条件,导致 index == length 时仍进入循环,产生越界访问及未定义行为。


小测验

问题 1
编写短程序,用循环输出下列 vector 的所有元素:

#include <iostream>
#include <vector>

int main()
{
    std::vector arr{ 4, 6, 7, 3, 8, 2, 1, 9 };

    // 在此添加代码

    return 0;
}

输出应为:

4 6 7 3 8 2 1 9

[显示解答]

问题 2
修改上一题答案,使下列程序可编译并保持相同输出:

#include <iostream>
#include <vector>

// 在此实现 printArray()

int main()
{
    std::vector arr{ 4, 6, 7, 3, 8, 2, 1, 9 };

    printArray(arr); // 用函数模板打印数组

    return 0;
}

[显示解答]

问题 3
在前题基础上完成以下需求:

  1. 提示用户输入 1 到 9 之间的值;若输入不符,则重复提示,直至合法。若用户输入数字后有冗余内容,忽略之。
  2. 打印数组。
  3. 编写函数模板,在数组中查找该值:找到则返回其下标,否则返回合适值。
  4. 若找到,输出值及其下标;未找到则输出值及“未找到”。

关于处理无效输入,参见课程 9.5 —— std::cin 与无效输入处理。

两次示例运行如下:

Enter a number between 1 and 9: d
Enter a number between 1 and 9: 6
4 6 7 3 8 2 1 9
The number 6 has index 1
Enter a number between 1 and 9: 5
4 6 7 3 8 2 1 9
The number 5 was not found

[显示解答]

问题 4
加分题:修改上一程序,使其支持非整型数值的 std::vector,如
std::vector arr{ 4.4, 6.6, 7.7, 3.3, 8.8, 2.2, 1.1, 9.9 };
[显示解答]

问题 5
编写函数模板,用于找出 std::vector 中的最大值。若 vector 为空,则返回元素类型的默认值。

以下代码应能运行:

int main()
{
    std::vector data1 { 84, 92, 76, 81, 56 };
    std::cout << findMax(data1) << '\n';

    std::vector data2 { -13.0, -26.7, -105.5, -14.8 };
    std::cout << findMax(data2) << '\n';

    std::vector<int> data3 { };
    std::cout << findMax(data3) << '\n';

    return 0;
}

并输出:

92
-13
0

[显示提示] [显示解答]

问题 6
在课程 8.10 —— for 语句 的测验中,我们实现了数字 3、5、7 的 FizzBuzz 游戏。本次测验请按以下规则实现:

  • 仅被 3 整除的数打印 “fizz”。
  • 仅被 5 整除的数打印 “buzz”。
  • 仅被 7 整除的数打印 “pop”。
  • 仅被 11 整除的数打印 “bang”。
  • 仅被 13 整除的数打印 “jazz”。
  • 仅被 17 整除的数打印 “pow”。
  • 仅被 19 整除的数打印 “boom”。
  • 被多个上述数整除时,按对应顺序拼接单词。
  • 不能被任何上述数整除则直接打印该数。

使用 std::vector 保存除数,另一 std::vector<std::string_view> 保存单词。若两数组长度不等,程序应断言。输出前 150 个数。

[显示提示] [显示提示]

前 21 次迭代预期输出:

1
2
fizz
4
buzz
fizz
pop
8
fizz
buzz
bang
fizz
jazz
pop
fizzbuzz
16
pow
fizz
boom
buzz
fizzpop

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"