11.2 — 数组(第二部分)

警告

本课程已弃用,并已替换为第 17 章后半部分(从课程 17.7 -- C 风格数组简介开始)的更新课程。

本课程继续讨论在课程 17.7 -- C 风格数组简介中开始的数组。

初始化固定数组

数组元素被视为普通变量,因此,它们在创建时不会被初始化。

“初始化”数组的一种方法是逐个元素进行

int prime[5]; // hold the first 5 prime numbers
prime[0] = 2;
prime[1] = 3;
prime[2] = 5;
prime[3] = 7;
prime[4] = 11;

然而,这很麻烦,尤其是当数组变大时。此外,它不是初始化,而是赋值。如果数组是 const,则赋值不起作用。

幸运的是,C++ 提供了一种更方便的方式,通过使用初始化列表来初始化整个数组。以下示例使用与上面相同的值初始化数组

int prime[5]{ 2, 3, 5, 7, 11 }; // use initializer list to initialize the fixed array

如果列表中有比数组能容纳的更多初始化器,编译器将生成错误。

但是,如果列表中有比数组能容纳的更少初始化器,则剩余元素被初始化为 0(或者 0 转换为非整数基本类型的任何值——例如,double 的 0.0)。这称为零初始化

以下示例显示了其作用

#include <iostream>

int main()
{
    int array[5]{ 7, 4, 5 }; // only initialize first 3 elements

    std::cout << array[0] << '\n';
    std::cout << array[1] << '\n';
    std::cout << array[2] << '\n';
    std::cout << array[3] << '\n';
    std::cout << array[4] << '\n';

    return 0;
}

这会打印

7
4
5
0
0

因此,要将数组的所有元素初始化为 0,可以这样做

int array[5]{};          // Initialize all elements to 0
double array[5] {};      // Initialize all elements to 0.0
std::string array[5] {}; // Initialize all elements to an empty string

如果省略初始化列表,则元素未初始化,除非它们是自初始化的类类型。

int array[5];         // uninitialized (since int doesn't self-initialize)
double array[5];      // uninitialized (since double doesn't self-initialize)
std::string array[5]; // Initialize all elements to an empty string

最佳实践

显式初始化数组(即使元素类型是自初始化的)。

省略长度

如果使用初始化列表初始化固定元素数组,编译器可以为您确定数组的长度,并且您可以省略显式声明数组的长度。

以下两行是等效的

int array[5]{ 0, 1, 2, 3, 4 }; // explicitly define the length of the array
int array[]{ 0, 1, 2, 3, 4 }; // let the initializer list set length of the array

这不仅节省了打字,还意味着您不必在以后添加或删除元素时更新数组长度。

数组和枚举

数组的一个主要文档问题是整数索引未向程序员提供有关索引含义的任何信息。考虑一个有 5 个学生的班级

constexpr int numberOfStudents{5};
int testScores[numberOfStudents]{};
testScores[2] = 76;

testScores[2] 代表谁?不清楚。

这可以通过设置一个枚举来解决,其中一个枚举器映射到每个可能的数组索引

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    int testScores[max_students]{}; // allocate 5 integers
    testScores[stan] = 76;

    return 0;
}

通过这种方式,每个数组元素代表什么就更清楚了。请注意,已添加一个名为 max_students 的额外枚举器。此枚举器在数组声明期间使用,以确保数组具有适当的长度(因为数组长度应比最大索引大一)。这对于文档目的很有用,并且因为如果添加另一个枚举器,数组将自动调整大小

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    wendy, // 5
    max_students // 6
};

int main()
{
    int testScores[max_students]{}; // allocate 6 integers
    testScores[stan] = 76; // still works

    return 0;
}

请注意,此“技巧”仅在您不手动更改枚举器值的情况下才有效!

数组和枚举类

枚举类没有隐式转换为整数,因此如果尝试以下操作

enum class StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    wendy, // 5
    max_students // 6
};

int main()
{
    int testScores[StudentNames::max_students]{}; // allocate 6 integers
    testScores[StudentNames::stan] = 76;

    return 0;
}

您将收到编译器错误。这可以通过使用 static_cast 将枚举器转换为整数来解决

int main()
{
    int testScores[static_cast<int>(StudentNames::max_students)]{}; // allocate 6 integers
    testScores[static_cast<int>(StudentNames::stan)] = 76;

    return 0;
}

然而,这样做有点麻烦,因此最好在命名空间内使用标准枚举

namespace StudentNames
{
    enum StudentNames
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        wendy, // 5
        max_students // 6
    };
}

int main()
{
    int testScores[StudentNames::max_students]{}; // allocate 6 integers
    testScores[StudentNames::stan] = 76;

    return 0;
}

将数组传递给函数

虽然将数组传递给函数乍一看就像传递普通变量一样,但在底层,C++ 对数组的处理方式不同。

当普通变量按值传递时,C++ 将参数的值复制到函数参数中。因为参数是副本,所以更改参数的值不会更改原始参数的值。

然而,由于复制大型数组可能非常昂贵,C++ 在将数组传递给函数时会复制数组。相反,会传递实际数组。这会产生允许函数直接更改数组元素值的副作用!

以下示例说明了此概念

#include <iostream>

void passValue(int value) // value is a copy of the argument
{
    value = 99; // so changing it here won't change the value of the argument
}

void passArray(int prime[5]) // prime is the actual array
{
    prime[0] = 11; // so changing it here will change the original argument!
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}

int main()
{
    int value{ 1 };
    std::cout << "before passValue: " << value << '\n';
    passValue(value);
    std::cout << "after passValue: " << value << '\n';

    int prime[]{ 2, 3, 5, 7, 11 }; // type deduced as int prime[5]
    std::cout << "before passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << '\n';
    passArray(prime);
    std::cout << "after passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << '\n';

    return 0;
}
before passValue: 1
after passValue: 1
before passArray: 2 3 5 7 11
after passArray: 11 7 5 3 2

在上面的示例中,main() 中的 value 未更改,因为函数 passValue() 中的参数 value 是函数 main() 中变量 value 的副本,而不是实际变量。然而,由于函数 passArray() 中的参数 array 是实际数组,passArray() 能够直接更改元素的值!

发生这种情况的原因与 C++ 中数组的实现方式有关,这是一个我们将在课程 17.8 -- C 风格数组衰变中重新讨论的主题。目前,您可以将其视为语言的一个怪癖。

另外,如果您想确保函数不修改传递给它的数组元素,可以将数组设为 const

// even though prime is the actual array, within this function it should be treated as a constant
void passArray(const int prime[5])
{
    // so each of these lines will cause a compile error!
    prime[0] = 11;
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}

确定数组的长度

可以使用 `` 头文件中的 `std::size()` 函数来确定数组的长度。

这是一个例子

#include <iostream>
#include <iterator> // for std::size

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << std::size(array) << " elements\n";

    return 0;
}

这会打印

The array has: 8 elements

请注意,由于 C++ 将数组传递给函数的方式,这对于已传递给函数的数组将起作用!

#include <iostream>
#include <iterator>

void printSize(int array[])
{
    std::cout << std::size(array) << '\n'; // Error
}

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << std::size(array) << '\n'; // will print the length of the array
    printSize(array);

    return 0;
}

std::size() 可以与其他类型的对象(如 std::array 和 std::vector)一起使用,如果您尝试在已传递给函数的固定数组上使用它,它将导致编译错误!请注意,std::size 返回一个无符号值。如果您需要有符号值,您可以强制转换结果,或者从 C++20 开始,使用 std::ssize()(代表有符号大小)。

std::size() 是在 C++17 中添加的。如果您使用的是 C++11 或 C++14,则可以使用此函数代替

#include <iostream>

template <typename T, std::size_t N>
constexpr std::size_t length(const T(&)[N]) noexcept
{
	return N;
}

int main() {

	int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
	std::cout << "The array has: " << length(array) << " elements\n";

	return 0;
}

在旧代码中,您可能会看到使用 sizeof 运算符计算长度。sizeof 不像 std::size() 那么容易使用,而且您需要注意一些事项。

sizeof 运算符可以用于数组,它将返回数组的总大小(数组长度乘以元素大小)。

#include <iostream>

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << sizeof(array) << '\n'; // will print the size of the array multiplied by the size of an int
    std::cout << sizeof(int) << '\n';

    return 0;
}

在具有 4 字节整数和 8 字节指针的机器上,它打印

32
4

(如果您的类型大小不同,您可能会得到不同的结果)。

一个巧妙的技巧:我们可以通过将整个数组的大小除以数组元素的大小来确定固定数组的长度

#include <iostream>

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << " elements\n";

    return 0;
}

这打印了

The array has: 8 elements

这是如何工作的?首先,请注意,整个数组的大小等于数组长度乘以元素大小。更简洁地说:数组大小 = 数组长度 * 元素大小。

使用代数,我们可以重新排列这个方程:数组长度 = 数组大小 / 元素大小。sizeof(array) 是数组大小,sizeof(array[0]) 是元素大小,所以我们的方程变成 数组长度 = sizeof(array) / sizeof(array[0])。有时使用 `*array` 而不是 `array[0]`(我们在17.9 -- 指针算术和下标中讨论 `*array` 是什么)。

请注意,这仅在数组是固定长度数组且您在声明该数组的同一函数中执行此技巧时才有效(我们将在本章的未来课程中详细讨论此限制为何存在)。

当 sizeof 用于已传递给函数的数组时,它不会像 std::size() 那样出错。相反,它返回指针的大小。

#include <iostream>

void printSize(int array[])
{
    std::cout << sizeof(array) / sizeof(array[0]) << '\n';
}

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << sizeof(array) / sizeof(array[0]) << '\n';
    printSize(array);

    return 0;
}

再次假设 8 字节指针和 4 字节整数,这打印

8
2

作者注

如果尝试在传递给函数的数组上使用 sizeof(),配置正确的编译器应打印警告。

main() 中的计算是正确的,但 printSize() 中的 sizeof() 返回 8(指针的大小),8 除以 4 是 2。

因此,在使用 sizeof() 对数组时要小心!

注意:在常见用法中,“数组大小”和“数组长度”这两个术语最常用于指代数组的长度(除了我们上面向您展示的技巧之外,数组的大小在大多数情况下没有用)。

数组越界索引

请记住,长度为 N 的数组具有元素 0 到 N-1。那么,如果您尝试使用超出此范围的下标访问数组会发生什么?

考虑以下程序

int main()
{
    int prime[5]{}; // hold the first 5 prime numbers
    prime[5] = 13;

    return 0;
}

在此程序中,我们的数组长度为 5,但我们尝试将一个素数写入第 6 个元素(索引 5)。

C++ 会进行任何检查以确保您的索引对于数组长度有效。因此,在上面的示例中,值 13 将被插入到内存中,该内存是第 6 个元素(如果存在的话)的位置。当发生这种情况时,您将获得未定义行为——例如,这可能会覆盖另一个变量的值,或者导致您的程序崩溃。

虽然这种情况发生得较少,但 C++ 也允许您使用负索引,并产生类似的不良结果。

规则

使用数组时,请确保您的索引在数组范围内有效!

测验

  1. 声明一个数组,用于存储一年中每天的最高温度(精确到小数点后一位)(假设一年有 365 天)。将数组中的每个元素初始化为 0.0。
  2. 设置一个枚举,其中包含以下动物的名称:鸡、狗、猫、大象、鸭子和蛇。将枚举放在命名空间中。定义一个数组,其中包含每种动物的元素,并使用初始化列表将每个元素初始化为该动物的腿数。

编写一个 main 函数,使用枚举器打印大象的腿数。

测验答案

  1. 显示答案
  1. 显示答案
%缺少课程 ID 83 的查找%
guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
490 评论
最新
最早 最多投票
内联反馈
查看所有评论