章节目录

多维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数组:

1
int ttt[3][3];

现在我们有了一个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
int array[3][5] {};

二维数组和循环

对于一维数组,我们可以使用单个循环来迭代数组中的所有元素:

 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],这可能与您期望的字母顺序相反。


17.10 常量C样式字符串

上一节

17.12 多维std::array

下一节