多维C样式数组
本节阅读量:
考虑一个下棋游戏。棋盘是3×3网格,玩家轮流放置“X”和“O”符号。第一个连成一条线的人获胜。
虽然您可以将数据存储为9个单独的变量,但我们知道,当您有一个元素的多个实例时,最好使用数组:
1
|
int ttt[9]; // c样式的 int 数组 (0 = 空, 1 = 玩家 1 的棋子, 2 = 玩家 2 的棋子)
|
这定义了一个C样式的数组,其中9个元素在内存中按顺序排列。我们可以将这些元素想象为一行值,如下所示:
1
|
// ttt[0] ttt[1] ttt[2] ttt[3] ttt[4] ttt[5] ttt[6] ttt[7] ttt[8]
|
数组的维数是选择元素所需的索引的个数。仅包含一维的数组称为一维数组。上面的ttt是一维数组的示例,因为可以使用单个索引来选择元素(例如ttt[2])。
但请注意,一维数组看起来不像我们的棋盘,棋盘存在于二维中。
二维数组
在前面的课程中,我们注意到数组的元素可以是任何对象类型。这意味着数组的元素类型可以是另一个数组!定义这样的数组很简单:
1
|
int a[3][5]; // 有 3 个 长度为 5 数组 的数组
|
这被称为二维数组,因为它有两个下标。
对于二维数组,可以方便地将第一个(左)下标视为选择行,而第二个(右)下标则视为选择列。从概念上讲,我们可以想象这个二维数组的布局如下:
1
2
3
4
|
// 列 0 列 1 列 2 列 3 列 4
// a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] 行 0
// a[1][0] a[1][1] a[1][2] a[1][3] a[1][4] 行 1
// a[2][0] a[2][1] a[2][2] a[2][3] a[2][4] 行 2
|
要访问二维数组的元素,需使用两个下标:
1
|
a[2][3] = 7; // a[行][列], 行 = 2 and 列 = 3
|
因此,对于前面的游戏棋盘,我们可以如下定义2d数组:
现在我们有了一个3×3的元素网格,我们可以使用行和列索引轻松地操作它!
多维数组
具有多个维度的数组称为多维数组。
C++甚至支持维度超过2的多维数组:
1
|
int threedee[4][4][4]; // 4x4x4 数组
|
例如,Minecraft中的地形被划分为16x16x16块(称为块段)。
支持维度大于3的数组,但很少有人会使用到。
二维数组如何在内存中布局
内存是线性的(一维),因此多维数组实际上存储为元素的顺序列表。
以下数组存储在内存中有两种可能的方法:
1
2
3
4
|
// 列 0 列 1 列 2 列 3 列 4
// a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] 行 0
// a[1][0] a[1][1] a[1][2] a[1][3] a[1][4] 行 1
// a[2][0] a[2][1] a[2][2] a[2][3] a[2][4] 行 2
|
C++使用行主顺序,其中元素按顺序在行中放置,从左到右,从上到下排序:
1
|
[0][0] [0][1] [0][2] [0][3] [0][4] [1][0] [1][1] [1][2] [1][3] [1][4] [2][0] [2][1] [2][2] [2][3] [2][4]
|
其他一些语言(如Fortran)使用列主顺序,元素按列顺序从上到下、从左到右放置在内存中:
1
|
[0][0] [1][0] [2][0] [0][1] [1][1] [2][1] [0][2] [1][2] [2][2] [0][3] [1][3] [2][3] [0][4] [1][4] [2][4]
|
在C++中,初始化数组时,元素按行主顺序初始化。在遍历数组时,最有效的方法是按元素在内存中的布局顺序访问它们。
初始化二维数组
要初始化二维数组,最容易使用的是嵌套大括号,每组数字表示一行:
1
2
3
4
5
6
|
int array[3][5]
{
{ 1, 2, 3, 4, 5 }, // 行 0
{ 6, 7, 8, 9, 10 }, // 行 1
{ 11, 12, 13, 14, 15 } // 行 2
};
|
尽管某些编译器允许您省略内部大括号,但出于可读性目的,我们强烈建议您无论如何都包括它们。
使用内部大括号时,缺少的初始值设定项将被初始化为零值:
1
2
3
4
5
6
|
int array[3][5]
{
{ 1, 2 }, // 行 0 = 1, 2, 0, 0, 0
{ 6, 7, 8 }, // 行 1 = 6, 7, 8, 0, 0
{ 11, 12, 13, 14 } // 行 2 = 11, 12, 13, 14, 0
};
|
初始化多维数组可以省略最左侧的长度:
1
2
3
4
5
6
|
int array[][5]
{
{ 1, 2, 3, 4, 5 },
{ 6, 7, 8, 9, 10 },
{ 11, 12, 13, 14, 15 }
};
|
在这种情况下,编译器可以进行数学运算,从初始值设定项的数量中找出最左边的长度。
不是最左侧的长度,不允许进行省略:
1
2
3
4
5
|
int array[][] // 不合法
{
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 }
};
|
与普通数组一样,多维数组可以直接初始化为零值,如下所示:
二维数组和循环
对于一维数组,我们可以使用单个循环来迭代数组中的所有元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include <iostream>
int main()
{
int arr[] { 1, 2, 3, 4, 5 };
// 使用长度的for循环进行遍历
for (std::size_t i{0}; i < std::size(arr); ++i)
std::cout << arr[i] << ' ';
std::cout << '\n';
// 使用基于范围的循环进行遍历
for (auto e: arr)
std::cout << e << ' ';
std::cout << '\n';
return 0;
}
|
对于二维数组,我们需要两个循环:一个选择行,另一个选择列。
对于两个循环,我们还需要确定哪个循环将是外循环,哪个将是内循环。按照元素在内存中的布局顺序访问它们是最有效的。由于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
|
#include <iostream>
int main()
{
int arr[3][4] {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }};
// 使用长度的两层for循环
for (std::size_t row{0}; row < std::size(arr); ++row) // std::size(arr) 返回行的个数
{
for (std::size_t col{0}; col < std::size(arr[0]); ++col) // std::size(arr[0]) 返回列的个数
std::cout << arr[row][col] << ' ';
std::cout << '\n';
}
// 基于范围的两层循环
for (const auto& arow: arr) // 获取每一个行
{
for (const auto& e: arow) // 获取行中的每一个元素
std::cout << e << ' ';
std::cout << '\n';
}
return 0;
}
|
二维数组示例
让我们看一个二维数组的实际示例:
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
31
32
33
|
#include <iostream>
int main()
{
constexpr int numRows{ 10 };
constexpr int numCols{ 10 };
// 声明 10x10 数组
int product[numRows][numCols]{};
// 计算乘法表
// 这里没有计算0行,0列,任何数乘0都是0
for (std::size_t row{ 1 }; row < numRows; ++row)
{
for (std::size_t col{ 1 }; col < numCols; ++col)
{
product[row][col] = static_cast<int>(row * col);
}
}
for (std::size_t row{ 1 }; row < numRows; ++row)
{
for (std::size_t col{ 1 }; col < numCols; ++col)
{
std::cout << product[row][col] << '\t';
}
std::cout << '\n';
}
return 0;
}
|
该程序为1和9(包括1和9)之间的所有值计算并打印乘法表。请注意,打印表时,for循环从1开始,而不是从0开始。这是为了省略打印0列和0行,这只是一堆0!输出如下:
1
2
3
4
5
6
7
8
9
|
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
|
笛卡尔坐标与数组索引
在几何学中,笛卡尔坐标系通常用于描述物体的位置。在二维中,我们有两个坐标轴,通常称为“x”和“y”。“x”是水平轴,“y”是垂直轴。
在二维中,对象的笛卡尔位置可以描述为{x,y}对,其中x坐标和y坐标是指示对象在x轴右侧的距离和在y轴上方的距离的值。有时y轴会翻转(以便y坐标描述某物在y轴下方的距离)。
现在,让我们看看C++中的2d数组布局:
1
2
3
4
|
// 列 0 列 1 列 2 列 3 列 4
// a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] 行 0
// a[1][0] a[1][1] a[1][2] a[1][3] a[1][4] 行 1
// a[2][0] a[2][1] a[2][2] a[2][3] a[2][4] 行 2
|
这也是一个二维坐标系,其中元素的位置可以描述为[行][列](其中列轴翻转)。
虽然这些坐标系中的每一个都很容易单独理解,但从笛卡尔{x,y}到数组索引[行][列]的转换有点违反直觉。
关键的见解是,笛卡尔系统中的x坐标描述了在数组索引系统中选择的列。相反,y坐标描述正在选择的行。因此,{x,y}笛卡尔坐标转换为[y][x]数组坐标,这与我们可能预期的相反!
这会产生如下所示的2d循环:
1
2
3
4
|
for (std::size_t y{0}; y < std::size(arr); ++y) // 外层是行 / y
{
for (std::size_t x{0}; x < std::size(arr[0]); ++x) // 内层是列 / x
std::cout << arr[y][x] << ' '; // 先按 y (行) , 然后是 x (列)
|
注意,在这种情况下,我们将数组索引为[y][x],这可能与您期望的字母顺序相反。