16-6 数组与循环(to do)

在本章的导论课(16.1——容器与数组导论)中,我们介绍了当存在大量相关独立变量时出现的可扩展性挑战。本节课将重新探讨该问题,并阐述数组如何帮助我们优雅地解决此类难题。


重新审视变量可扩展性挑战

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

以下是使用独立变量解决此问题的方案:

#include <iostream>

int main()
{
    // allocate 5 integer variables (each with a different name)
    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;
}

image

这样更好——我们大幅减少了定义的变量数量,平均值计算的除数现在直接取自数组的长度。

但平均值计算仍存在问题,因为我们不得不手动逐个列出每个元素。由于必须显式列出每个元素,当前的平均值计算仅适用于元素数量与列出数量完全一致的数组。若要支持其他长度的数组求平均值,就需要为每种数组长度编写新的平均值计算逻辑。

我们真正需要的是某种无需显式列出每个元素就能访问数组元素的方法。


数组与循环

在之前的课程中,我们提到数组下标不必是常量表达式——它们可以是运行时表达式。这意味着我们可以使用变量的值作为索引。

另请注意,前例中计算平均值所用的数组索引构成递增序列:0, 1, 2, 3, 4。因此,若能将某个变量依次赋值为0、1、2、3、4,便可直接用该变量替代字面量作为数组索引。而我们早已掌握实现方法——使用for循环。

相关内容:
我们在第8.10课——for语句中讲解过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 from 0 to length-1
        average += testScore[index];                      // add the value of element with index `index`
    average /= static_cast<int>(length);                  // calculate the average

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

    return 0;
}

image

这应该相当直观。索引从0开始,testScore[0]被加到平均值中,索引递增到1。testScore[1]被加到平均值中,索引递增到2。最终当索引递增到5时,index < length为假,循环终止。

此时循环已将 testScore[0]、testScore[1]、testScore[2]、testScore[3] 和 testScore[4] 的值累加到平均值中。

最后通过将累加值除以数组长度计算最终平均值。

该方案在可维护性方面堪称理想:循环迭代次数由数组长度决定,循环变量负责所有数组索引操作,无需手动列举每个数组元素。

若需增删测试分数,只需修改数组初始化项数量,其余代码无需改动即可正常运行!

按特定顺序访问容器中每个元素的过程称为遍历容器traversal:traversing the container。遍历常被称为迭代iteration,即对容器进行迭代操作iterating through

作者注:
由于容器类使用类型 size_t 表示长度和索引,本节课我们将采用相同做法。关于带符号长度和索引的使用,将在后续第16.7节——数组、循环与符号挑战解决方案中讨论。


模板、数组和循环解锁可扩展性

数组提供了一种存储多个对象的方式,无需为每个元素命名。

循环提供了一种遍历数组的方式,无需显式列出每个元素。

模板提供了一种对元素类型进行参数化的方式。

模板、数组和循环共同作用,使我们能够编写操作元素容器的代码,无论容器中的元素类型或数量如何!

为进一步说明,让我们重写上述示例,将平均值计算重构为函数模板:

#include <iostream>
#include <vector>

// Function template to calculate the average of the values in a std::vector
template <typename T>
T calculateAverage(const std::vector<T>& arr)
{
    std::size_t length { arr.size() };

    T average { 0 };                                      // if our array has elements of type T, our average should have type T too
    for (std::size_t index{ 0 }; index < length; ++index) // iterate through all the elements
        average += arr[index];                            // sum up all the elements
    average /= static_cast<int>(length);                  // divide by count of items (integral in nature)

    return average;
}

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

    std::vector class2 { 93.2, 88.6, 64.2, 81.0 };
    std::cout << "The class 2 average is: " << calculateAverage(class2) << '\n'; // calc average of 4 doubles

    return 0;
}

这将输出:

image

在上例中,我们创建了函数模板 calculateAverage(),该函数接受任意元素类型和任意长度的 std::vector,并返回平均值。在 main() 中,我们演示了该函数在调用 5 个 int 元素的数组或 4 个 double 元素的数组时同样有效!

calculateAverage() 适用于任何支持函数内部运算符(operator+=(T)、operator/=(int))的类型 T。若尝试使用不支持这些运算符的 T,编译器在编译实例化函数模板时将报错。

您可能疑惑为何将长度转换为 int 类型而非 T 类型。计算平均值时,我们需将总和除以元素个数。元素个数本身是整数值,因此从语义上讲,除以 int 类型更为合理。


数组与循环的应用场景

既然我们已掌握使用循环遍历元素容器的方法,接下来让我们看看容器遍历最常见的用途。通常我们遍历容器是为了实现以下四种操作之一:

  • 基于现有元素值计算新值(例如求平均值、求值之和)。
  • 搜索特定元素(如查找精确匹配项、统计匹配次数、找出最大值)。
  • 对每个元素执行操作(如输出所有元素、将所有元素乘以2)。
  • 重新排序元素(如按升序排列元素)。

前三种操作较为简单,可通过单层循环遍历数组,对每个元素进行相应检查或修改。

而容器元素的重新排序则复杂得多,通常需要嵌套循环实现。虽然可手动完成,但通常更推荐使用标准库中的现成算法。我们将在后续章节讨论算法时对此进行详细说明。


数组与偏移量错误

使用索引遍历容器时,必须确保循环执行次数正确。偏移量错误(即循环体执行次数过多或过少)极易发生。

通常,使用索引遍历容器时,我们会将索引初始化为0,并循环至索引值小于长度。

新手程序员有时会误将索引值小于等于长度作为循环条件。这将导致当索引值等于长度时循环仍继续执行,从而引发越界索引访问并导致行为未定义。


测验时间

问题 #1

编写一个简短程序,使用循环将下列向量的元素打印到屏幕上:

#include <iostream>
#include <vector>

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

    // Add your code here

    return 0;
}

输出应如下所示:

4 6 7 3 8 2 1 9

image

显示解决方案

#include <iostream>
#include <vector>

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

    for (std::size_t index{ 0 }; index < arr.size(); ++index)
    {
        std::cout << arr[index] << ' ';
    }

    if (arr.size() > 0)
        std::cout << '\n';

    return 0;
}

问题 #2

请更新先前测验解决方案中的代码,使以下程序能够编译并产生相同输出:

#include <iostream>
#include <vector>

// Implement printArray() here

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

    printArray(arr); // use function template to print array

    return 0;
}

image

显示答案

#include <iostream>
#include <vector>

template <typename T>
void printArray(const std::vector<T>& arr)
{
    for (std::size_t index{ 0 }; index < arr.size(); ++index)
    {
        std::cout << arr[index] << ' ';
    }

    if (arr.size() > 0)
        std::cout << '\n';
}

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

    printArray(arr);

    return 0;
}

问题 #3

根据测验 2 的解决方案,执行以下操作:

  • 要求用户输入 1 到 9 之间的数值。若用户未输入 1 到 9 之间的数值,则反复要求输入整数直至满足要求。若用户输入数字后跟有无关内容,则忽略无关输入。
  • 打印数组。
  • 编写函数模板,用于在数组中搜索用户输入的数值。若数值存在于数组中,返回该元素的索引;若不存在,返回适当值。
  • 若找到数值,输出数值及索引;若未找到,输出数值并提示未找到。

第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

显示答案:

#include <iostream>
#include <limits>
#include <vector>

template <typename T>
void printArray(const std::vector<T>& arr)
{
    for (std::size_t index{ 0 }; index < arr.size(); ++index)
    {
        std::cout << arr[index] << ' ';
    }

    if (arr.size() > 0)
        std::cout << '\n';
}

template <typename T>
int findIndex(const std::vector<T>& arr, T val)
{
    for (std::size_t index{ 0 }; index < arr.size(); ++index)
    {
        if (arr[index] == val)
            return static_cast<int>(index);
    }

    return -1; // -1 is not a valid index, so we can use it as an error return value
}


int getValidNumber()
{
    // First, read in valid input from user
    int num {};
    do
    {
        std::cout << "Enter a number between 1 and 9: ";
        std::cin >> num;

        // if the user entered an invalid character
        if (!std::cin)
            std::cin.clear(); // reset any error flags

        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // ignore any extra characters in the input buffer (regardless of whether we had an error or not)

    } while (num < 1 || num > 9);

    return num;
}

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

    int num { getValidNumber() };

    printArray(arr);

    int index { findIndex(arr, num) };

    if (index != -1)
        std::cout << "The number " << num << " has index " << index << '\n';
    else
        std::cout << "The number " << num << " was not found\n";

    return 0;
}

问题 #4

附加题:修改之前的程序,使其能够处理包含非整数值的 std::vector,例如:

std::vector arr{ 4.4, 6.6, 7.7, 3.3, 8.8, 2.2, 1.1, 9.9 };

显示答案:

#include <iostream>
#include <limits>
#include <vector>

template <typename T>
void printArray(const std::vector<T>& arr)
{
    for (std::size_t index{ 0 }; index < arr.size(); ++index)
    {
        std::cout << arr[index] << ' ';
    }

    if (arr.size() > 0)
        std::cout << '\n';
}

template <typename T>
int findIndex(const std::vector<T>& arr, T val)
{
    for (std::size_t index{ 0 }; index < arr.size(); ++index)
    {
        if (arr[index] == val)
            return static_cast<int>(index);
    }

    return -1; // -1 is not a valid index, so we can use it as an error return value
}

// Passing in low and high allows the compiler to infer the type of the input we want
template <typename T>
T getValidNumber(std::string_view prompt, T low, T high)
{
    // First, read in valid input from user
    T val {};
    do
    {
        std::cout << prompt;
        std::cin >> val;

        // if the user entered an invalid character
        if (!std::cin)
            std::cin.clear(); // reset any error flags

        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // ignore any extra characters in the input buffer (regardless of whether we had an error or not)

    } while (val < low || val > high);

    return val;
}

int main()
{
    std::vector arr{ 4.4, 6.6, 7.7, 3.3, 8.8, 2.2, 1.1, 9.9 };

    auto num { getValidNumber("Enter a number between 1 and 9: ", 1.0, 9.0) };

    printArray(arr);

    int index { findIndex(arr, num) };

    if (index != -1)
        std::cout << "The number " << num << " has index " << index << '\n';
    else
        std::cout << "The number " << num << " was not found\n";

    return 0;
}

问题 #5

编写一个函数模板,用于查找 std::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

显示提示

提示:使用非循环变量来记录你迄今为止看到的最高值。

显示解答

#include <iostream>
#include <vector>

template <typename T>
T findMax(const std::vector<T>& arr)
{
    std::size_t length { arr.size() };

    if (length==0)
        return T{};

    T max { arr[0] }; // Set the max seen to the first element

    // Iterate through any remaining elements looking for a larger value
    for (std::size_t index{ 1 }; index < length; ++index)
    {
        if (arr[index] > max)
            max = arr[index];
    }

    return max;
}

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;
}

在此示例中,我们使用名为 max 的非循环变量来记录当前看到的最高分。初始化时将 max 赋值为数组首元素,确保其从有效值开始。虽然数值初始化看似便捷,但若数组仅含负值,此方法将导致函数返回错误值。

随后遍历数组每个元素,若发现高于当前最大值的分数,则将max更新为该值。因此max始终代表当前遍历范围内最高分。遍历结束时,max将存储整个数组的最高分,此时即可将其返回给调用方。


问题 #6

在第 8.10 课的测验中——关于 for 语句,我们实现了一个名为 FizzBuzz 的游戏,适用于数字 3、5 和 7。

在本测验中,请按以下规则实现该游戏:

仅能被 3 整除的数字应输出“fizz”。

仅能被 5 整除的数字应输出“buzz”。

仅能被 7 整除的数字应输出“pop”。

仅能被11整除的数字应输出“bang”。

仅能被13整除的数字应输出“jazz”。

仅能被17整除的数字应输出“pow”。

仅能被19整除的数字应输出“boom”。

若数字同时被多个上述数字整除,则应输出所有对应的整除词。

无法被上述任何数字整除的数字仅需输出数字本身。

使用 std::vector 存储因数,另用 std::vector 存储单词(类型为 std::string_view)。若数组长度不一致,程序应触发断言。生成 150 个数字的输出结果。

显示提示

提示:使用 sv 字面量后缀将单词转换为 std::string_view 类型,这样你就可以使用 CTAD 推断数组的类型。

显示提示

提示:使用嵌套的 for 循环来检查该数是否为其因数。我们在第 8.10 课——for 语句中讨论了嵌套的 for 循环。

以下是前 21 次迭代的预期输出:

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

显示答案

// h/t to reader Waldo for suggesting this quiz
#include <cassert>
#include <iostream>
#include <string_view>
#include <vector>

void fizzbuzz(int count)
{
	// We'll make these static so we only have to do initialization once
	static const std::vector divisors                { 3, 5, 7, 11, 13, 17, 19 };
	static const std::vector<std::string_view> words { "fizz", "buzz", "pop", "bang", "jazz", "pow", "boom" };
	assert(std::size(divisors) == std::size(words) && "fizzbuzz: array sizes don't match");

	// Loop through each number between 1 and count (inclusive)
	for (int i{ 1 }; i <= count; ++i)
	{
		bool printed{ false };

		// Check the current number against each possible divisor
		for (std::size_t j{ 0 }; j < divisors.size(); ++j)
		{
			if (i % divisors[j] == 0)
			{
				std::cout << words[j];
				printed = true;
			}
		}

		// If there were no divisors
		if (!printed)
			std::cout << i;

		std::cout << '\n';
	}
}

int main()
{
	fizzbuzz(150);

	return 0;
}

提示:
由于除数和单词是常量数组,最好将其声明为 constexpr —— 但 std::vector 无法实现这一点。在此处使用 constexpr std::array 会更合适。我们将在第 17.1 课std::array 简介 中讲解 std::array。

posted @ 2026-01-05 18:01  游翔  阅读(20)  评论(0)    收藏  举报