需要预先了解:


1. 数组

数组(Array):同一类型数据的连续内存集合。

数组中的所有元素的类型都相同,并且长度固定,C++中可以用[]运算符来定义数组:

// 类型 数组名[数组长度]
int arr[5];         // 长度为5的 int 类型的整数数组
char str[100];      // 长度为100的 char 类型变量的字符数组(字符串)

1.1 通过[]运算符来访问数组元素

数组的下标索引从0开始:

int main()
{
    int arr[5];
    
    arr[0] = 10;
    arr[4] = 50;
    arr[5] = 100;    // 错误,arr的下标范围是0~4,没有5
}

1.2 常量数组

可以在定义数组的时候用const关键字来修饰数组元素,使得数组的每一个元素的值都不能被修改。

#include <iostream>

int main()
{
    const int arr[5] = {1, 2, 3, 4, 5};     // 定义一个 const 数组
    const char str[] = "Hello";             // 定义一个 const 字符数组(字符串)

    arr[0] = 10;        // 错误,不能修改 const 数组的元素

    return 0;
}

1.3 数组长度与变长数组扩展(VLA)

标准 C++ 要求定义数组时,数组的长度必须是一个编译时就可以确定的常量表达式,不能是一个变量或者运行时才能确定的值。

#include <iostream>

int main()
{
    const int N = 5;        // N 是编译时就能确定的 const 常量
    constexpr int M = 10;   // M 是编译时就能确定的 constexpr 常量

    int x; std::cin >> x;        // x 是一个变量,运行时才能确定其值
    const int y = x;        // y 是运行时才能确定的 const 常量

    int arr[5];             // 正确,5 是字面量 
    int arr1[N];            // 正确,N 是编译时常量
    int arr2[M];            // 正确,M 是编译时常量
    int arr3[x];            // 错误,x 是变量
    int arr4[y];            // 错误,y 是运行时常量

    return 0;
}

GCC 和 Clang 等编译器支持一种称为变长数组(Variable Length Array,VLA)的扩展特性,允许在定义数组时使用一个运行时才能确定的变量作为数组长度,因此直接编译运行上面的代码也是可以的。

我们在编译指令中加上 -pedantic-errors 选项来启用严格的标准兼容性检查,再进行编译,就会得到报错:

code.cpp: In function 'int main()':
code.cpp:14:9: error: ISO C++ forbids variable length array 'arr3' [-Wvla]
   14 |     int arr3[x];            // 错误,x 是变量
      |         ^~~~
code1.cpp:15:9: error: ISO C++ forbids variable length array 'arr4' [-Wvla]
   15 |     int arr4[y];            // 错误,y 是运行时常量
      |         ^~~~

1.4 数组的初始化

1. 完全显式初始化

用 {} 来依次排列出要初始化的元素值,中间用逗号分隔。

int arr[5] = {1, 2, 3, 4, 5};       // 定义并初始化一个长度为 5 的整数数组

2. 部分初始化

{} 中列出的元素值的个数小于数组长度,未列出的元素会被自动初始化为 0。

int arr1[5] = {1, 2};
// 将 arr[0] 初始化为 1
// 将 arr[1] 初始化为 2
// arr[2]、arr[3]、arr[4] 被初始化为 0

int arr2[5] = {};       // 空大括号会自动将 arr2 中的所有元素初始化为 0

3. 不指定数组长度,由编译器进行推导

在定义数组时,[] 中不指定数组长度,编译器会根据 {} 中列出的元素值的个数来推导数组的长度。

int arr[] = {1, 2, 3, 4, 5};       // 定义并初始化一个整数数组,长度由编译器推导为 5

1.5 二维数组与多维数组

二维数组

定义二维数组时,需要指定两个维度的大小,即类型 数组名[行数][列数];,例如:

int matrix[3][4];

初始化时,可以有以下方式:

int matrix1[3][4] = {    // 按行初始化,每行元素用花括号括起来
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

int matrix2[3][4] = {    // 连续初始化,编译器会自动进行填充
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
};

int matrix3[3][4] = {   // 部分初始化,未初始化的元素被赋予默认类型
    {1, 2}, {5}
};

访问二维数组的元素时,也需要行索引和列索引,二者均从0开始:

int matrix[3][4] = {1, 2, 3, 4};
int x = matrix[1][2];

2. 数组的内存布局

数组在内存中是连续存储的,每个元素占用相同大小的内存空间。

比如对于 int arr[5],数组的每个元素都占用四个字节的空间,我们用其中第一个字节的地址来表示整个元素的地址。假设 arr[0] 的地址是 0x12345678ABCD1234,那么数组的内存布局如下:

arr[0] --- 0x12345678ABCD1234
arr[1] --- 0x12345678ABCD1238
arr[2] --- 0x12345678ABCD123C
arr[3] --- 0x12345678ABCD1240
arr[4] --- 0x12345678ABCD1244

2.1 多维数组的内存布局

行优先规则

行优先规则(Row Major):存放多维元素时,最靠右的下标最先连续变化;靠左的下标变化更慢。 对于二维数组arr[i][j],内存地址从小到大的填充顺序:

  • 先固定行 i=0,把所有 j 依次放完
  • 再 i=1,放所有 j
  • 再 i=2,放所有 j

二维数组的内存布局

核心特征为:所有元素连续存储在一块无空隙的内存。

给定二维数组int arr[3][4],假设int类型变量占4个字节,&arr[0][0] == 0x1000:

元素 线性偏移元素序号 内存地址
arr[0][0] 0 0x1000
arr[0][1] 1 0x1004
arr[0][2] 2 0x1008
arr[0][3] 3 0x100C
arr[1][0] 4 0x1010
arr[1][1] 5 0x1014
arr[1][2] 6 0x1018
arr[1][3] 7 0x101C
arr[2][0] 8 0x1020
arr[2][1] 9 0x1024
arr[2][2] 10 0x1028
arr[2][3] 11 0x102C

不难得到:

&arr[i][j] = &arr[0][0] + (i * 列数 + j) * sizeof(int);

更高维数组的内存布局

类似地,三维数组arr[i][j][k]的内存地址计算公式为:

&arr[i][j][k] = &arr[0][0][0] + (i * 行数 * 列数 + j * 列数 + k) * sizeof(int);

N维数组arr[i1][i2][i3]...[iN]的内存地址计算公式为:

&arr[i1][i2][i3]...[iN] = &arr[0][0][0]...[0] + (i1 * D2 * D3 *... * DN + i2 * D3 * D4 * ... * DN + ... + iN-1 * DN + iN) * sizeof(元素类型);