V2.0
C++基础教程<br />——<br />C语言部分卷2数组<br/>节2二维数组<br/><br/>最新版本V2.0
<br>王道C++团队<br/>COPYRIGHT ⓒ 2021-2024. 王道版权所有概述二维数组的本质数学角度内存角度二维数组的使用声明初始化为什么在声明时可省略行,不能省略列?二维数组元素的读/写二维数组元素的循环遍历The End
Gn!
在C语言中,数组可以是任意维度的。但最常用的还是一维数组,偶尔会用到一下二维数组,更高维度的数组则建议不要考虑,几乎不会使用。
所以本小节当中,我们一起来探讨一下二维数组。
Gn!
理解C语言中二维数组的本质是掌握其应用的基础前提。要深入了解二维数组的本质,我们可以从以下两个角度出发:
Gn!
从数学角度看,二维数组就是数学中的矩阵。矩阵具有行和列的结构,其中每个元素都可以通过其行和列的索引进行定位。
例如,声明初始化一个arr[5][9]的二维数组,就可以把该二维数组看成是一个具有5行9列的矩阵。
如下图所示:
当我们使用下标arr[i][j]访问二维数组元素时,可以看作是访问矩阵的第i行和第j列的元素。
所以在声明二维数组时,我们把arr[i][j]中的i称之为行数,j称之为列数。
Gn!
从内存的角度看,二维数组其实是一个“元素是一维数组的一维数组”。
在内存中,它是一系列连续的一维数组。每个一维数组(或称为“子数组”)都有相同的长度,并在内存中连续存储。
例如,int arr[3][4]实际上是3个长度为4的int数组的连续存储。
这种存储方式被称为“行主序”,因为最外层的数组索引(即行索引)变化的频率低于内部的数组索引(列索引)。
因此,arr[0][0]、arr[0][1]、arr[0][2]...是连续存储的,然后是arr[1][0]、arr[1][1]....
如下图所示:
因此,二维数组的数据在物理存储上仍然是线性的,但为了更好地组织和访问,我们逻辑上将其视为二维结构。
总之在C语言中,理解二维数组有两个关键视角:数学与内存布局。
从数学角度看,将其视为矩阵。这非常有助于我们学习二维数组的语法以及各种操作。
从内存布局角度,要知道二维数组本质还是使用连续的内存空间存储,相当于把多个一维数组拼在一起使用。明确这一点,使得我们可以更好的理解C语言中多维数组的访问模式,以及写出性能更好的代码。
结合这两个角度,我们不仅能更有效地操作二维数组,还能更好地优化二维数组的相关代码。
Gn!
使用一个二维数组和一维数组在流程上并没有太大区别,依然需要经历声明和初始化的过程。如果是函数体内部声明,那么此二维数组仍然是一个局部变量数组。
Gn!
二维数组的声明格式为:
二维数组的声明语法
x1数据类型 数组名[行数][列数];
例如,想要声明一个3行4列的int二维数组,可以这样写:
xxxxxxxxxx
11int arr[3][4];
如果是局部变量数组,那么仅声明的情况下,元素都是随机值。此时数组是不可用的,还需要经过初始化。
Gn!
在声明的同时,可以为二维数组的元素赋值初始化。初始化可以是部分的,也可以是完整的,主要有以下形式:
完整初始化:
二维数组完整初始化-语法示例
x1int arr[3][4] = {
2{1, 2, 3, 4},
3{5, 6, 7, 8},
4{9, 10, 11, 12}
5};
省略(矩阵)行数:
在初始化时,你可以不指定行数,让编译器根据提供的数据自动计算:
二维数组省略行数初始化-语法示例
x1int arr[][4] = {
2{1, 2, 3, 4},
3{5, 6, 7, 8},
4{9, 10, 11, 12}
5};
编译器会自动计算出这是一个3x4的二维数组(矩阵)。
部分初始化:
如果只初始化部分元素,其他未指定的元素会默认初始化为0(对于基本数据类型)。
二维数组部分初始化-语法示例
xxxxxxxxxx
41int arr[3][4] = {
2{1, 2},
3{5, 6}
4};
这里,只有arr[0][0]、arr[0][1]、arr[1][0]、arr[1][1]被初始化,其他元素的值都是0。(arr[2]的所有元素都是0)
初始化为0值:
如果希望所有元素都初始化为0值,则按照下列方式:
二维数组全部初始化0-语法示例
x1int arr[3][4] = {
2{ 0 },
3};
至少给出一个初始化式0,不能全部都省略。
同样的,除了上面给出的初始化形式,其余初始化形式都是错误的。不要随意尝试别的方式。
Gn!
通过上述初始化形式,我们不难发现在初始化二维数组的过程:
可以省略行,但是不能省略列
不仅不可以省略列,而且列必须明确一个唯一值。
从内存的角度考虑,这意味着:
初始化一个二维数组时,必须明确指出子数组(列)的长度,且每个子数组(列)的长度是一致的。
那么这是为什么呢?
这是因为二维数组实际上是一种特殊的一维数组,其中每个元素都是一个一维数组:
必须先连续存储完第一个一维数组,然后存储第二个,第三个...此时必须要知道每个数组长度是多少,才可能采用这种方式存储。
二维数组的访问机制,决定了每个一维数组的长度必须是一致的。
二维数组元素的随机访问机制
那么二维数组中的元素是如何进行随机访问的呢?
实际上你既然知道二维数组本质仍然是一个一维数组,那么这一点就非常简单了,还是利用寻址公式。
例如一个二维数组的声明如下:
xxxxxxxxxx
11int arr[3][4];
这意味着arr是一个有3个元素的数组,每个元素又是一个有4个整数的数组。在内存中,这个数组是这样存储的("行主序"):
当你尝试访问 arr[i][j]元素时,依然是随机访问的。对于二维数组,C 语言使用以下寻址公式计算其内存位置地址:
二维数组的寻址公式
xxxxxxxxxx
11address(arr[i][j]) = base_address + (i * cols_num) * sizeof_element + j * sizeof_element
其中:
base_address是数组的基地址,实际上就是二维数组中第一个一维数组的第一个元素的地址。同样的,二维数组名在内存中就代表这个地址。
cols_num是每行的元素数量,也就是列长(也就是一维数组的长度,这里是4)
sizeof_element是数组元素的大小(在这里是int的大小,一般是4)。
为了保证上述计算的准确性,每个子数组(或说每一行)的长度必须保持一致。如果每行的元素数量变得不一致,该公式就无法准确地计算内存地址,从而导致错误的数组元素访问。
这就是为什么在 C 语言的标准二维数组中,每个一维数组的长度必须相同的原因。
以上,相信你对二维数组的理解在这里会更上一层楼。
Gn!
当你声明并初始化一个二维数组后,就可以开始使用它了,这涉及到二维数组元素的读取和修改。
对于二维数组,你需要两个下标来定位一个特定的元素。你可以把这两个下标,理解为矩阵的行索引和列索引。
二维数组的元素读取和写入与一维数组相似,但需要两个下标:
二维数组元素的读/写-语法
x1数据类型 element = 数组名[row_index][col_index]; // 读取二维数组元素的值
2数组名[row_index][col_index] = value; // 将值赋给二维数组的元素
其中,row_index 是元素所在的行索引,而 col_index 是元素所在的列索引。在C语言中,行索引和列索引都是从0开始计数。value是你想赋给该元素的值,当然也可以写一个表达式。
注意事项:
二维数组要通过两个索引进行访问,这使得访问非法索引的可能性增加。因此,当操作二维数组时,要特别注意确保两个索引都是合法的,因为程序并不会自动帮你进行索引的有效性检查。
与一维数组类似,二维数组中的元素,如果没有经过初始化,它们的值是不确定的。直接访问这些未经初始化的元素可能会导致程序行为异常。因此,一旦声明了二维数组,建议尽快对其所有元素进行初始化操作。
Gn!
一维数组往往使用单层for循环遍历,那么二维数组就可以使用双层嵌套for循环来完成遍历。话虽如此,但我们还是需要思考里面的一些小细节:
如果外层循环遍历行,内层循环遍历列,这就是"行优先遍历"形式。
如果外层循环遍历列,内层循环遍历行,这就是"列优先遍历"形式。
我们应该怎么选择呢?
当然你首先要知道它们有啥区别:
行优先遍历,意味着先遍历完"矩阵"的一整行,再转而遍历下一行元素。也就是说,先遍历完二维数组中的第一个一维数组,再遍历第二个一维数组...
列优先遍历,意味着先遍历完"矩阵"的一整列,再转而遍历下一列元素。也就是说,先访问第一个子数组的第一个元素,再访问第二个子数组的第一个元素...先把所有子数组的第一个元素访问,然后再访问所有子数组的第二个元素...
很明显,列优先遍历给我们的感觉是"跳着"将整个二维数组访问完毕。实际上由于内存连续物理结构的客观条件,以及连续访问可以利用缓存加速的性能优势,行优先的效率是更高的,是更推荐的遍历方式。
假设我们有一个二维数组"int arr[3][4];"。如果想遍历每个元素,示例代码如下:
二维数组遍历-示例代码
xxxxxxxxxx
2312
345
6int main() {
7int arr[ROWS][COLS] = {
8{1, 2, 3, 4},
9{5, 6, 7, 8},
10{9, 10, 11, 12}
11};
12// 外循环遍历行
13for (int i = 0; i < ROWS; i++) {
14// 内循环遍历列
15for (int j = 0; j < COLS; j++) {
16// 访问并打印每个元素
17printf("%d ", arr[i][j]);
18}
19// 每行结束后换行
20printf("\n");
21}
22return 0;
23}
注意代码当中的宏定义,当然你可以使用宏函数来计算数组长度,但对于二维数组这会稍嫌麻烦一些,如下:
二维数组遍历-示例代码2
xxxxxxxxxx
1212...
3
4int ROWS = ARRAY_LENGTH(arr);
5int COLS = ARRAY_LENGTH(arr[0]);
6
7for (int i = 0; i < ROWS; i++) {
8for (int j = 0; j < COLS; j++) {
9printf("%d ", arr[i][j]);
10}
11printf("\n");
12}
循环过程中,要注意边界值,谨防不合法的索引出现。