11.2 - 数组(第二部分)
Key Takeaway
- 使用list-initialization方式初始化数组
int prime[5]{ 2,3, 5,7, 11 };
- 初始化列表中的元素个数超过数组能容纳的个数时会报错,少于时剩下的会被初始化为 0
- 初始化一个全 0 数组:
int prime[5]{};
- 如果省略初始化列表,则数组元素为未初始化状态,除非它们是
class
类型的。 - 不论数组元素可以在没有初始化列表时会不会被默认初始化,都应当显式地初始化数组。
- 使用列表初始化对数组初始化时,编译器可以自动计算数组长度,因此不需要声明数组的长度。
- 可以添加了一个名为
max_XXX
的额外枚举值。此枚举值在数组声明期间使用,以确保数组具有适当的长度(因为数组长度应该比最大索引大 1)。这对于代码可读性非常重要,当我们添加新的枚举值时,数组将自动调整大小: - 限定作用域枚举(枚举类)并不能隐式地转换为对应的整型
- 使用普通枚举类型并将其放置在某个作用域中,比使用枚举类更好。
- 数组是传地址不是传值,所以如果你想确保一个函数不会修改传递给它的数组元素,可以将数组设置为
const
<iterator>
头文件中的std: size()
函数可以用来判断数组的长度,但不能用于数组被当做参数传递的情形(C++17)sizeof
操作符可以用于数组,它会返回数组的大小(数组长度乘以数组元素的大小),使用数组大小除以数组第一个元素的大小可以得到数组长度- 将
sizeof
应用于被传入函数的数组时,不会像std:: size()
一样导致编译错误,取而代之的是,它会返回指针的长度。
书接上文( 11.1 - 数组(第一部分))。
固定数组初始化
数组元素会被当做普通变量来对待,因此,它们在创建时默认是不初始化的。
“初始化”数组的方式之一,是逐个元素进行初始化:
1 2 3 4 5 6 |
|
不过,这么做很麻烦,几乎不可能用来初始化非常长的数组。不仅如此,与其说这是在初始化,倒不如说是在赋值。如果数组是 const
类型的话,赋值是行不通的。
幸好,我们可以使用初始化值列表(initializer list)来初始化整个数组。下面例子中初始化的数组和上一个例子的”初始化“结果是完全一样的:
1 |
|
如果列表中初始化值的数量超过了数组所能容纳的数量,编译器将会报告一个错误。
但是,如果列表中初始化器的数量少于数组所能容纳的数量,则剩余的元素将被初始化为 0
(或者,0
会被转换其他非整型的值——例如,对于 double
类型,0
会转换为 0.0
)。这被称为零初始化(zero initialization)。
请看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
打印:
1 2 3 4 5 |
|
因此,要将数组中的所有元素初始化为 0,你可以这样做:
1 2 3 4 5 6 7 8 |
|
如果省略初始化列表,则数组元素为未初始化状态,除非它们是 class
类型的。
1 2 3 4 5 6 7 8 |
|
最佳实践
不论数组元素可以在没有初始化列表时会不会被默认初始化,都应当显式地初始化数组。
省略数组长度
如果您使用初始化列表初始化一个固定长度数组,编译器可以自动计算出数组的长度,所以你可以省略数组长度的声明。
下面两行是等价的:
1 2 |
|
这么做的好处可不仅仅是方便,同时可以避免你在修改了元素后还要更新数组长度声明。
数组和枚举
对于数组来说,践行代码即文档的最大障碍,就是整数索引无法向程序员提供关于索引含义的任何信息。假设一个有 5
个学生的班级:
1 2 3 |
|
testScores[2]
表示的是谁的分数?我们看不出来!
解决这个问题的办法是使用枚举类型,每个枚举变量对应一个数组索引:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这样,每个数组元素所代表的含义就更加清晰了。注意,我们添加了一个名为 max_students
的额外枚举值。此枚举值在数组声明期间使用,以确保数组具有适当的长度(因为数组长度应该比最大索引大 1)。这对于代码可读性非常重要,当我们添加新的枚举值时,数组将自动调整大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
注意,这个技巧只有在你不需要手动修改枚举值对应的数值时才有用。
数组和枚举类
枚举类(enum class)并不能隐式地转换为对应的整型,所以如果你尝试编写下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
你会得到一个编译错误。为了解决这个问题,你可以使用 static_cast
将枚举值转换为整型。
1 2 3 4 5 6 7 |
|
不过,这么做显然很麻烦。所以,使用普通枚举类型并将其放置在某个作用域中,比使用枚举类更好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
将数组传递给函数
虽然乍一看向函数传递数组和传递普通变量好像没什么区别,但在本质上,C++对传递数组的处理是不同的。
普通变量通过按值传递的方式传入函数——C++将实参的值复制到函数形参中。因为形参是一个副本,改变形参的值并不会改变原始实参的值。
但是,由于复制大型数组的开销非常大,所以 C++在将数组传递给函数时不会复制数组。相反,它会传递数组本身。这样做的副作用是函数可以直接更改数组元素的值!
下面的例子说明了这个概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
1 2 3 4 |
|
在上面的例子中,value
没有在 main()
被中改变,因为 passValue()
函数中的参数值是 main()
函数中变量值的副本,而不是实际的变量。然而,由于 passArray()
函数中的数组是数组本身,passArray()
能够直接改变元素的值!
上述现象背后的原因,和 C++ 实现数组的方式有关,我们会在 11.8 - 指针和数组中进行详细介绍。现在你可以暂时认为这是语言中的一个”怪异“特性。
注意,如果你想确保一个函数不会修改传递给它的数组元素,你可以将数组设置为 const
:
1 2 3 4 5 6 7 8 9 10 |
|
确定数组长度
<iterator>
头文件中的 std: size()
函数可以用来判断数组的长度。
例如:
1 2 3 4 5 6 7 8 9 10 |
|
打印:
1 |
|
注意,由于 C++ 函数传递数组的特殊性,上面的方法不能用于数组被传递到函数中的情形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
std:: size()
也可以用于其他类型的对象(例如 std:: array
和 std:: vector
),而且如果你对被传入函数的数组使用 std: size()
时,会产生编译报错。注意,std:: size
会返回的是一个无符号值,如果你需要使用有符号值,则要么将其转换为有符号值,或者使用 C++20 中提供的 std:: ssize()
(signed size)。
译者注
std:: size()
是在 C++17 中引入的,所以如果你使用的是旧版本的编译器,那么只好使用 sizeof
运算符。sizeof
运算符使用起来不如 std: size()
方便,而且有些地方要特别注意。如果你使用的是兼容 C++17 的编译器, 可以直接跳到数组访问越界一节。
sizeof
操作符可以用于数组,它会返回数组的大小(数组长度乘以数组元素的大小)。
1 2 3 4 5 6 7 8 9 10 |
|
在整型大小为 4 字节的机器上:
1 2 |
|
经典技巧:将数组的大小除以数组元素的大小,就可以得到数组的长度。
1 2 3 4 5 6 7 8 9 |
|
打印:
1 |
|
这是怎么回事?首先,请注意,整个数组的大小等于数组的长度乘以元素的大小,即 array size = array length * element size
。
公式变形一下就可以得到:array length = array size / element size
。sizeof(array)
即数组的大小,而 sizeof(array[0])
则是数组元素的大小。带入公式得到数组 length = sizeof(array) / sizeof(array[0])
。我们通常使用数组元素 0 作为来计算数组元素的大小,因为无论数组长度是多少,第一个元素总是存在的。
将 sizeof
应用于被传入函数的数组时,不会像 std:: size()
一样导致编译错误,取而代之的是,它会返回指针的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
同样假设 8 字节指针和 4 字节整数,输出结果为:
1 2 |
|
作者注
如果试图对传递给函数的数组使用 sizeof(),正确配置的编译器应该打印一个警告。
在 main()
中计算数组长度是正确的,但是 printSize()
中的 sizeof()
返回了 8
(指针的大小),8
除以 4
等于 2
。
因此,在数组上使用 sizeof()
时要小心!
注意:在通常的用法中,术语“数组大小”和“数组长度”都是最常用于指数组的长度(数组的大小在大多数情况下是没有用的,除了我们上面展示的技巧)。
数组访问越界
记住,长度为 N
的数组,其下标从 0
到 N-1
。如果你试图访问的数组下标,在这个范围之外会发生什么?
考虑以下程序:
1 2 3 4 5 6 7 |
|
在这个程序中,数组长度为 5
,但我们试图将第 6 个元素(索引 5)写入 prime
数组。
C++ 不会检查下标是否合法。所以在上面的例子中,13 的值会被插入到内存中(第 6 个元素位置的内存可用的话)。当这种情况发生时,您将得到未定义的行为——例如,这可能会覆盖另一个变量的值,或导致程序崩溃。
尽管不常见,但在 C++中也可以使用负数索引,这也会导致错误。
法则
当使用数组时,确保你的索引在有效范围内