重要的事情说三遍:
数组是一系列相同类型的元素,这些元素放置在连续的内存位置中,可以通过在唯一标识符上添加索引来单独引用。
这意味着,例如,五个 int
类型的值可以声明为一个数组,而不需要声明 5 个不同的变量(每个变量都有自己的标识符)。使用数组,这五个 int
值存储在连续的内存位置中,所有五个值都可以使用相同的标识符和适当的索引进行访问。
例如,一个包含 5 个 int
类型整数值的数组 foo
可以表示为:
其中每个空白面板代表数组的一个元素。在这种情况下,这些都是 int
类型的值。这些元素从 0 到 4 进行编号,0 是第一个,4 是最后一个。在 C++ 中,数组的第一个元素总是编号为 0(而不是 1),无论其长度如何。
像普通变量一样,数组在使用前必须声明。C++ 中数组的典型声明是:
type name [elements];
其中 type
是有效类型(例如 int
、float
),name
是有效标识符,而 elements
字段(总是用方括号 []
括起来)指定数组的长度,以元素的数量表示。
因此,具有五个 int
类型元素的 foo
数组可以声明为:
int foo [5];
注意:方括号 []
内的 elements
字段表示数组中的元素数量,必须是一个常量表达式,因为数组是静态内存块,其大小必须在编译时确定,在程序运行之前。
初始化数组(Initializing arrays)
默认情况下,本地作用域(例如,在函数内声明)的普通数组未初始化。这意味着在数组声明时,其元素没有设置为任何特定值;它们的内容是不确定的。
但是,可以在声明时通过大括号 {} 显式地将数组元素初始化为特定值。例如:
int foo [5] = { 16, 2, 77, 40, 12071 };
这个语句声明了一个数组,可以表示为:
数组中的值不能超过数组元素的数量。例如,上述例子中的 foo
被声明为具有 5 个元素(由方括号 []
内的数字指定),大括号 {}
中包含了 5 个值,每个元素一个值。如果声明的值较少,剩余的元素将被设置为默认值(对于基本类型来说,默认值为 0)。例如:
int bar [5] = { 10, 20, 30 };
将创建一个如下的数组:
初始化器甚至可以没有值,只有大括号:
int baz [5] = { };
这将创建一个包含五个 int
值的数组,每个值初始化为 0:
当为数组提供初始化值时,C++ 允许方括号 []
为空。在这种情况下,编译器会自动假定数组的大小与大括号 {}
中包含的值数量匹配:
int foo [] = { 16, 2, 77, 40, 12071 };
在这个声明之后,数组 foo
的长度为 5,因为我们提供了 5 个初始化值。
最后,C++ 的演变也导致了数组的统一初始化。因此,不再需要在声明和初始化器之间使用等号。这两种语句是等价的:
int foo[] = { 10, 20, 30 };
int foo[] { 10, 20, 30 };
静态数组和直接在命名空间中声明的数组(在任何函数外部)总是被初始化的。如果没有指定显式初始化器,所有元素都将默认初始化(对于基本类型来说,初始化为零)。
访问数组的值
数组中任何元素的值可以像相同类型的普通变量一样访问。语法是:
name[index]
根据前面的示例,foo
有 5 个元素,每个元素的类型为 int
,可以用以下名称来引用每个元素:
例如,以下语句将值 75 存储在 foo
的第三个元素中:
foo [2] = 75;
例如,以下语句将 foo
的第三个元素的值复制到名为 x
的变量中:
x = foo[2];
因此,表达式 foo[2]
本身就是一个 int
类型的变量。
请注意,foo
的第三个元素表示为 foo[2]
,因为第一个是 foo[0]
,第二个是 foo[1]
,因此第三个是 foo[2]
。同理,最后一个元素是 foo[4]
。因此,如果我们写 foo[5]
,我们将访问 foo
的第六个元素,从而实际上超出了数组的大小。
在 C++ 中,语法上可以超出数组的有效索引范围。这可能会产生问题,因为访问超出范围的元素不会在编译时引发错误,但可能会在运行时引发错误。允许这样做的原因将在引入指针的章节中看到。
此时,重要的是要清楚地区分方括号 []
与数组相关的两种用途。它们执行两项不同的任务:一个是声明数组时指定数组的大小;另一个是访问具体数组元素时指定索引。不要将这两种方括号 []
的可能用途与数组混淆。
int foo[5]; // 声明一个新数组
foo[2] = 75; // 访问数组的一个元素
主要区别在于,声明之前有元素的类型,而访问时没有。
一些其他有效的数组操作:
foo[0] = a;
foo[a] = 75;
b = foo [a+2];
foo[foo[a]] = foo[2] + 5;
例如:
// 数组示例
#include <iostream>
using namespace std;
int foo [] = {16, 2, 77, 40, 12071};
int n, result=0;
int main ()
{
for ( n=0 ; n<5 ; ++n )
{
result += foo[n];
}
cout << result;
return 0;
}
多维数组(Multidimensional arrays)
多维数组可以描述为“数组的数组”。例如,一个二维数组可以被想象为由相同数据类型的元素组成的二维表。
jimmy
表示一个 3 行 5 列的 int
类型的二维数组。C++ 的语法是:
int jimmy [3][5];
例如,引用垂直第二行和水平第四列的元素的方式是:
jimmy[1][3]
(记住数组索引总是从零开始)。
多维数组不限于两个索引(即两个维度)。它们可以包含任意多个索引。但要小心:数组所需的内存量随着每个维度的增加而呈指数增长。例如:
char century [100][365][24][60][60];
声明了一个 char
类型的数组,每秒一个元素。这相当于超过 30 亿个 char
!所以这个声明将消耗超过 3GB 的内存!
最终,多维数组对于程序员来说只是一个抽象,因为通过乘以其索引可以用一个简单的数组实现相同的结果:
int jimmy [3][5]; // 相当于
int jimmy [15]; // (3 * 5 = 15)
唯一的区别是,使用多维数组时,编译器会自动记住每个假想维度的深度。以下两段代码产生完全相同的结果,但一个使用二维数组,另一个使用简单数组:
// 多维数组
#define WIDTH 5
#define HEIGHT 3
int jimmy [HEIGHT][WIDTH];
int n,m;
int main ()
{
for (n=0; n<HEIGHT; n++)
for (m=0; m<WIDTH; m++)
{
jimmy[n][m]=(n+1)*(m+1);
}
}
// 伪多维数组
#define WIDTH 5
#define HEIGHT 3
int jimmy [HEIGHT * WIDTH];
int n,m;
int main ()
{
for (n=0; n<HEIGHT; n++)
for (m=0; m<WIDTH; m++)
{
jimmy[n*WIDTH+m]=(n+1)*(m+1);
}
}
以上两段代码均不会在屏幕上产生任何输出,但都以如下方式为名为 jimmy
的内存块赋值:
注意,代码使用定义的常量来表示宽度和高度,而不是直接使用它们的数值。这使得代码更具可读性,并且可以在一个地方轻松进行更改。
作为参数的数组
在某些时候,我们可能需要将数组作为参数传递给函数。在 C++ 中,不能将数组代表的整个内存块直接作为参数传递给函数。但可以传递它的地址。实际上,这几乎具有相同的效果,而且操作更快、更高效。
要接受一个数组作为函数参数,可以将参数声明为数组类型,但使用空括号,省略数组的实际大小。例如:
void procedure (int arg[])
这个函数接受一个类型为“int
数组”的参数 arg
。为了将一个声明为:
int myarray [40];
数组传递给这个函数,只需这样调用:
procedure (myarray);
这里有一个完整的示例:
// 作为参数的数组
#include <iostream>
using namespace std;
void printarray (int arg[], int length) {
for (int n=0; n<length; ++n)
cout << arg[n] << ' ';
cout << '\n';
}
int main ()
{
int firstarray[] = {5, 10, 15};
int secondarray[] = {2, 4, 6, 8, 10};
printarray (firstarray,3);
printarray (secondarray,5);
}
在上面的代码中,第一个参数 (int arg[]
) 接受任何元素为 int
的数组,无论其长度。因此,我们包括了第二个参数,该参数告诉函数我们传递给它的每个数组的长度。这样可以使打印数组的 for 循环知道要在传递的数组中迭代的范围,而不会超出范围。
在函数声明中,还可以包含多维数组。三维数组参数的格式是:
base_type[][depth][depth]
例如,一个以多维数组作为参数的函数可以是:
void procedure (int myarray[][3][4])
注意,第一个括号 []
留空,而后面的括号指定其各自维度的大小。这是必要的,以便编译器能够确定每个附加维度的深度。
某种程度上,作为参数传递数组总是会丢失一个维度。其背后的原因是,由于历史原因,数组不能直接复制,因此实际上传递的是指针。这是新手程序员常犯的错误来源。尽管清楚理解指针(在后续章节中解释)会有很大帮助。
库数组(Library arrays)
上述数组是直接作为语言特性实现的,继承自 C 语言。它们是一个很好的特性,但由于其复制限制和容易退化为指针,可能过于优化。
为了解决内置数组的某些问题,C++ 提供了一个替代的数组类型作为标准容器。它是头文件 <array>
中定义的类型模板(实际上是类模板)。
容器是库特性,不在本教程范围内,因此这里不会详细解释类。只需说它们的操作方式与内置数组相似,不同的是它们允许被复制(实际上是一个昂贵的操作,复制整个内存块,因此要谨慎使用),并且只有在明确要求时才会退化为指针(通过其成员 data
)。
仅举例说明,这里有两个版本的相同示例,一个使用本章中描述的语言内置数组,另一个使用库中的容器:
// 语言内置数组
#include <iostream>
using namespace std;
int main()
{
int myarray[3] = {10, 20, 30};
for (int i = 0; i < 3; ++i)
++myarray[i];
for (int elem : myarray)
cout << elem << '\n';
}
// 库数组
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<int, 3> myarray {10, 20, 30};
for (int i = 0; i < myarray.size(); ++i)
++myarray[i];
for (int elem : myarray)
cout << elem << '\n';
}
如你所见,这两种数组都使用相同的语法来访问其元素:myarray[i]
。除此之外,主要区别在于数组的声明,以及为库数组包含了一个额外的头文件。注意到,库数组访问大小也很方便。