本节视频链接:点击这里
1、多维数组的定义和结构
int nMatrix[6][10];
这个二维数组nMatrix包含了6个一维数组,每个一维数组的长度为10,总计有60个int型数据元素。它的逻辑结构如下图所示:
nMatrix | ||||||||||
nMatrix[0] | nMatrix[0][0] | nMatrix[0][1] | nMatrix[0][2] | nMatrix[0][3] | nMatrix[0][4] | nMatrix[0][5] | …... | …... | …... | ….. |
nMatrix[1] | nMatrix[1][0] | nMatrix[1][1] | nMatrix[1][2] | nMatrix[1][3] | nMatrix[1][4] | …... | …... | …... | …... | ….. |
nMatrix[2] | nMatrix[2][0] | nMatrix[2][1] | nMatrix[2][2] | nMatrix[2][3] | …... | …... | …... | …... | ….. | ….. |
nMatrix[3] | nMatrix[3][0] | nMatrix[3][1] | nMatrix[3][2] | ….. | …... | …... | ….. | ….. | ….. | ….. |
nMatrix[4] | nMatrix[4][0] | nMatrix[4][1] | ….. | ….. | ….. | ….. | ….. | ….. | ….. | ….. |
nMatrix[5] | nMatrix[5][0] | nMatrix[5][1] | ….. | ….. | ….. | ….. | ….. | ….. | ….. | ….. |
0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 | 0x0008 | 0x0009 | 0x0010 | 0x0011 | 0x0012 |
nArray[0] | nArray[1] | nArray[2] |
一个二维数组,相当于一个一维数据每一个元素都是另一个一维数组。二维数组首先从首地址开始按顺序存储第一行的数据,然后在第一行最末尾元素的下一个内存单元开始保存第二行数据,依次类推,直至保存完最后一行。从二维数组的下标来看,按照内存单元的向后依次遍历的顺序,最优先变化的是数组最右边的下标,即,变化顺序为:nMatrix[0][0],nMatrix[0][1],nMatrix[0][2]…...nMatrix[0][9],nMatrix[1][0],nMatrix[1][1], nMatrix[1][2]…...nMatrix[1][9], nMatrix[2][0], nMatrix[2][1]…...nMatrix[2][9], nMatrix[3][0]……….nMatrix[5][9]。C语言中二维数组的这种存储结构称为“行主序”。
2、二维数组和指针
我们已经知道,一个一级指针可以指向一个一维数组,或者说,可以将一个一维数组名赋值给一个一级指针。同样道理,如果我们定义了一个二维数组,就可以定义一个二级指针指向这个二维数组:
int nMatrix[6][10]; int **ppMatrix = nMatrix;
这个值该如何理解?首先我们研究一下二维数组中的数值和地址的关系。在这个二维数组中,第一行的元素分别为 nMatrix[0][0], nMatrix[0][1] … nMatrix[0][9],并且这些元素也构成一个数组。对比一下,数组a[0]、a[1]和a[2]的首地址为a,那么我们可以推测这第一行的各个元素的首地址是 nMatrix[0]。事实上正是如此,一个二维数组中,每一行元素都是一个一维数组,这一个一维数组的首地址/数组名就是二维数组名+左索引构成。例如,第0行的首地址为nMatrix[0],第1行的首地址为nMatrix[1],第n-1行的首地址为nMatrix[n-1]。这n个首地址表示的是指向一维数组的地址,可以用一级指针指向。即:
int *pRow = nMatrix[0];
不仅如此,这n个首地址的保存位置也构成一个一维数组。通过观察着n个首地址的命名,我们可以得知,包含着n个首地址的一维数组的首地址实际上就是二维数组名nMatrix。由于每行首地址的数据类型为int *,那么由它们组成数组的首地址类型就是int **,于是我们可以定义一个二级指针指向这个地址,即:
int **ppMatrix = nMatrix;这也解释了本节开始时为什么可以定义二级指针指向二维数组名。
在定义了指向二维数组的指针之后,我们就可以通过该指针对二维数组进行读写等操作。例如我们可以根据该指针获取每一行元素的首地址。以下几行代码实现是等价的:
int *pRow2 = ppMatrix[2]; int *pRow2 = *(ppMatrix + 2);
也可以直接获取二维数组中的某个元素:
int nRow1Col3 = ppMatrix[1][3]; int nRow1Col3 = *(ppMatrix[1] + 3); int nRow1Col3 = (*(ppMatrix+1))[3]; int nRow1Col3 = *(*(ppMatrix+1)+3);
int nMatrix[3][2] = { {9, 7, 5}, {6, 4, 2} };
3、三维和更高维度数组
事实上,C语言所支持的数组维度远不止二维数组,三维甚至更高维度的数组也是支持,甚至是很常用。经过了前面的知识,我们很容易归纳出多维数组的定义方法:
char cCubeArray[3][6][8]; //定义一个3层、6行、8列的三维数组 int nIntBuf[4][5][4][6]; //定义一个4块、5层、4行、6列的四维数组
以三维数组为例,对于三维数组进行遍历,使用3层循环结构即可。最右一层下标变化最快,越向左下标变化越慢。例如我们定义一个多维数组表示日期和时间,则可定义这样一个数组:
int nDateTimeValue[12][31][24][60][60];
这几个下标从左到右分别表示月、日、小时、分、秒。当表示秒的下标循环到60后,分钟向下+1;当分钟循环到60后,小时向下+1;当小时循环到24后,日的下标向下+1,依次类推。
for (int month = 0; month < 12; month++) { for(int day = 0; day < 31; day++) { for(int hour = 0; hour < 24; hour++) { for(int minute = 0;minute < 60; minute++) { for(int second = 0; second<60; second++) { printf(“Value of %d month, %d day, %d hour, %d minute, %d second: %d”, month, day, hour, minute, second, nDateTimeValue[month][day][hour][minute][second]); } } } } }
4、指针数组和指向数组的指针
int *ptArr1[6]; int (*ptArr2)[6];
我们需要理解清楚这二者是否相同,以及两个指针 ptArr1和 ptArr2分别代表的含义。这两个复杂的声明包含了间接运算符、括号运算符、下标运算符等,要理解这两种声明,首先需要清楚不同运算符的顺序。下标运算符[ ]和括号( )的优先级为1,为各种运算符最高;间接运算符优先级为2,次于下标和括号。因此对于声明 int *ptArr1[6],首先计算的是 ptArr1[6]部分,因此 ptArr1是一个长度为6的一维数组;然后间接运算符所代表的是数据类型,即为指向整型变量的指针。这种声明方式声明了一个指针数组,这是一个长度为6的一维数组,保存了6个整型变量指针,数据类型等同于int *。
另一种声明方式int (*ptArr2)[6],由于括号( )优先级最高,因此首先计算的是(*ptArr2)。从整个声明的格式上来看,(*ptArr2)是一个整型一维数组的数组名即一维数组的首地址。所以ptArr2是一个指向一维数组首地址的指针,数据类型等同于int **。因此,可以将二维数组名初始化给这个指针:
int (*ptArr2)[6] = nMatrix;
这种类型的指针定义和初始化创造了一个指向二维数组第一行的指针。对指针 ptArr2,其增加某个整数的作用是一行一行地移动指针。因为在定义时制定了指向的数组的长度,因此在移动时系统会根据指向数组的长度进行调整。所以我们需要注意的是,如果我们希望以此方式处理数组的数据,定义时数组的长度不要指定错误,也不要设为空,如同下面的样子:
int (*ptArr2)[] = nMatrix; //错误的用法
因为,编译器只有知道了第二个和后面各维的长度,才能在移动指针时确定每一步移动的实际长度,对各个下标进行求值。