天天看点

理解C语言——从小菜到大神的晋级之路(9)——多维数组1、多维数组的定义和结构2、二维数组和指针3、三维和更高维度数组4、指针数组和指向数组的指针

        一个数组中可以支持各种数据类型,那么一个数组中的每一个元素同样也可以是一个数组。对于上次提到的一维数组,其每个元素都是一个简单数据类型的对象,其结构如同一个一维的数据排列;对于一个二维数组,它的每一个元素都是一个一维数组,其形式如同一个二维的表格,表格的宽度是其中作为数据元素的一维数组的长度,高度是这样的一维数组的个数。简而言之,二维数组的结构是一个矩阵的形式。

        例如,我们声明下面这样的一个二维数组:

        这个二维数组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]

        由这样的结构我们也称作是一个“6行10列”的二维数组。由于这种结构特别适合表示图像和视频数据,这样的多维数组在多媒体处理等领域有着广泛的应用。

        我们知道,对于一维数组,其存储结构是一段连续的内存空间。比如一个一维数组int nArray[3],其存储结构为:

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语言中二维数组的这种存储结构称为“行主序”。

        用另外一个例子说明,如果某个数值按照一天中每一小时、每分钟不同来变化,就可以定义一个二维数组int nValue[24][60]。每过一分钟,最右边的下标就增1,当增长到60时,最右边的下标重新归0,左边的下标增1。

        需要说明的是,这里将nMatrix解释为6行10列是一种表示方法,将其解释为10行6列也是可行的,具体哪种方法更优最好依据表示的数据的意义来决定。不过无论哪种表示方法,都不可能改变二维数组的在内存中的存储结构。

        我们已经知道,一个一级指针可以指向一个一维数组,或者说,可以将一个一维数组名赋值给一个一级指针。同样道理,如果我们定义了一个二维数组,就可以定义一个二级指针指向这个二维数组:

        这个值该如何理解?首先我们研究一下二维数组中的数值和地址的关系。在这个二维数组中,第一行的元素分别为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个首地址表示的是指向一维数组的地址,可以用一级指针指向。即:

        不仅如此,这n个首地址的保存位置也构成一个一维数组。通过观察着n个首地址的命名,我们可以得知,包含着n个首地址的一维数组的首地址实际上就是二维数组名nMatrix。由于每行首地址的数据类型为int *,那么由它们组成数组的首地址类型就是int **,于是我们可以定义一个二级指针指向这个地址,即:

        这也解释了本节开始时为什么可以定义二级指针指向二维数组名。

      在定义了指向二维数组的指针之后,我们就可以通过该指针对二维数组进行读写等操作。例如我们可以根据该指针获取每一行元素的首地址。以下几行代码实现是等价的:

        也可以直接获取二维数组中的某个元素:

        二维数组的初始化方法的原理与一维数组原理类似。一维数组的初始化使用大括号实现,而二维数组也是如此,只是其内部每个元素也是一个用大括号初始化的一维数组:

      事实上,C语言所支持的数组维度远不止二维数组,三维甚至更高维度的数组也是支持,甚至是很常用。经过了前面的知识,我们很容易归纳出多维数组的定义方法:

        以三维数组为例,对于三维数组进行遍历,使用3层循环结构即可。最右一层下标变化最快,越向左下标变化越慢。例如我们定义一个多维数组表示日期和时间,则可定义这样一个数组:

        这几个下标从左到右分别表示月、日、小时、分、秒。当表示秒的下标循环到60后,分钟向下+1;当分钟循环到60后,小时向下+1;当小时循环到24后,日的下标向下+1,依次类推。

        当数组和指针相互结合的时候,程序的意义可能就会变得比较麻烦且难以理解。比如以下两种声明:

        我们需要理解清楚这二者是否相同,以及两个指针ptArr1和ptArr2分别代表的含义。这两个复杂的声明包含了间接运算符、括号运算符、下标运算符等,要理解这两种声明,首先需要清楚不同运算符的顺序。下标运算符[ ]和括号( )的优先级为1,为各种运算符最高;间接运算符优先级为2,次于下标和括号。因此对于声明int

*ptArr1[6],首先计算的是ptArr1[6]部分,因此ptArr1是一个长度为6的一维数组;然后间接运算符所代表的是数据类型,即为指向整型变量的指针。这种声明方式声明了一个指针数组,这是一个长度为6的一维数组,保存了6个整型变量指针,数据类型等同于int *。

      另一种声明方式int (*ptArr2)[6],由于括号( )优先级最高,因此首先计算的是(*ptArr2)。从整个声明的格式上来看,(*ptArr2)是一个整型一维数组的数组名即一维数组的首地址。所以ptArr2是一个指向一维数组首地址的指针,数据类型等同于int

**。因此,可以将二维数组名初始化给这个指针:

        这种类型的指针定义和初始化创造了一个指向二维数组第一行的指针。对指针ptArr2,其增加某个整数的作用是一行一行地移动指针。因为在定义时制定了指向的数组的长度,因此在移动时系统会根据指向数组的长度进行调整。所以我们需要注意的是,如果我们希望以此方式处理数组的数据,定义时数组的长度不要指定错误,也不要设为空,如同下面的样子:

        因为,编译器只有知道了第二个和后面各维的长度,才能在移动指针时确定每一步移动的实际长度,对各个下标进行求值。

继续阅读