二、二维数组
对于一个n维数组,其实质上还是一个一维数组,这个一维数组的每个元素又都是一个(n-1)维数组。。以此类推。
复杂的不去深究,就看二维数组a[m][n],实质是一个由m个元素组成的一维数组,每个元素又都是含n个元素的一维数组,这个二维数组共计m*n个元素。对于一个二维数组,它实质上是一个一维数组,但是是什么样的一维数组?这个一维数组的元素是什么?
对于int a[2][3],逻辑上是2行×3列的整型矩阵,在内存分布中为线性存储。其定义为:int a[2][3]={{1,2,3},{4,5,6}};
容易看出这是含有两个元素的一维数组,每个元素又都是一个含有3个整型数据的一维数组。即a[0]={1,2,3};a[1]={4,5,6};
上述废话那么多,其实就是一句话:多维数组实质上就是一维数组,知道这一点对后续的理解非常重要。
1、二维数组名
以上述int a[2][3]为例,定义上为二维数组,而实质上就是一维数组{a[0],a[1]}(其中a[0]={a[0][0],a[0][1],a[0][2]},a[1]={a[1][0],a[1][1],a[1][2]})。所以二维数组的数组名依然可以套用之前提到的一维数组名的两层含义:
第一层含义:表示的是整个数组,依然是看似废话可很有用的一句话。
第二层含义:表示一个地址,是等效的一维数组首元素的首地址。
那么与一维数组一样的问题:a,&a,&a[0][0]分别表示什么?设二维数组首地址为X,一样去分析:
(1)当a作为地址时(数组名的第二层含义),表示的是等效一维数组首元素的首地址,即a[0]的首地址;那么(a+1)呢?因为a对应的是一维数组元素,而这里,这个一维数组的元素又是一个3个整型数据组成的一维数组,所以a对应的单元大小为3*sizeof(int)=3*4=12个字节,即(a+1)=X+12.
(2)对于&a,因为a的第一层含义(表示整个数组),所以&a表示的是整个数组的首地址,那么(&a+1)呢?不再唠叨,因为a对应整个数组,所以单位大小为2*3*sizeof(int)=24Byte。(&a+1)=X+24.
(3)&a[0][0]当然为二维数组首元素的地址,(&a[0][0]+1)=X+1*sizeof(int)=X+4。
从上面描述,可以看出,虽然数值大小上a,&a,&a[0][0]是相等的,但含义不尽相同。下为示例验证:
#include<stdio.h> int main() { int a[2][3]={1,2,3,4,5,6}; printf("%p--%p--%p. ",a,&a,&a[0][0]); printf("%p--%p--%p. ",a+1,&a+1,&a[0][0]+1); getch(); return 0; }
2、二维数组的等效一维数组的数组元素
对于上述a[2][3],其等效一维数组元素分别为a[0]={1,2,3};a[1]={4,5,6};注意,这里只是逻辑上的一种描述写法,而非C代码。
这里数组元素本身又为一维数组,可以将a[0],a[1]分别看成这两个数组的数组名。即a[0]是{1,2,3}这个数组的数组名,a[1]是{4,5,6}这个数组的数组名。
那么依据前述数组名含义的描述,因为a[0]是{1,2,3}这个数组的数组名,我们初步判断,在表示地址这个含义上:所以a[0]是这个数组首元素的地址,即a[0]=&a[0][0],那么(a[0]+1)=X+1*sizeof(int)=X+4;&a[0]为a[0]这个数组的首地址,(&a[0]+1)=X+3*sizeof(int)=X+12。
同理(a[1]+1)=X+4;(&a[1]+1)=X+12。
通过下面这个示例也可以看出上面的初步判断是正确的(虽然这仅是充分而非必要条件的证明=。=)
#include<stdio.h> int main() { int a[2][3]={1,2,3,4,5,6}; printf("%p--%p--%p. ",a,&a,&a[0][0]); printf("%p--%p--%p. ",a[0],a[0]+1,&a[0]+1); getch(); return 0; }
3、指针在二维数组中的应用
3.1一级指针访问
对于int a[2][3];int *p;
有办法可以通过指针变量p去访问二维数组a的元素么?当然有,让p指向二维数组首地址就可以了。
那么究竟通过哪种方法去设置呢?是p=a;还是p=&a,或p=&a[0][0]甚至p=a[0]还是p=&a[0]呢?
通过下例测试可以发现,不管哪种方法,都可以运行出正确结果;但其中只有当p=&a[0][0]和p=a[0]时候编译不会提示warning信息。
其他方法产生的warning信息是因为p=X;这句左右值的地址类型不匹配而造成的,只是因为这是赋值语句,而且都是指针类型大小(4字节),所以不影响赋值操作,也就可以运行。具体以一个例子解释警告信息产生的原因:若用p=a;p是一个指向整型数据的指针变量,而a作为地址时候表示的是等效一维数组的首元素地址(该元素为包括3个整型数据的数组类型);赋值左右指向类型不一致,造成warning。
#include<stdio.h> int main() { int a[2][3]={1,2,3,4,5,6}; int *p; p=&a[0][0]; //自己去改a,&a,&a[0],a[0],看编译时候哪些会有warning信息 printf("%d--%p",*p,p+1); getch(); return 0; }
那么我们就用p=&a[0][0](或p=a[0])去设定p的指向。设定完成后如何访问呢?以访问a[i][j]为例,两种方法:一种对应于指针,以首地址为基准地址,加上偏移量,即(p+i*3+j);另外一种以下标方式p[i*3+j]这也不难理解。那么可以用p[i][j]么?我们理所当然认为可以。但自己测试一下吧,答案是不可以。
3.2数组指针的访问
对于二维数组,之前说到其实质为一个数组元素为一维数组的一维数组;那么如果有一个指针,可以指向一个数组;再以该指针为基准进行偏移,不就形成一个元素为一维数组的一维数组了么?文字说明总是很拗口。看示例解释:
假设Pointer X指向一个一维数组,那么(X+1)就是指向同等规模的下一个一维数组空间,(X+N)就是指向同等规模的第N个一维数组空间。
那么该如何表示一个指向一维数组的指针变量呢?变量定义时候格式都是如此:“类型说明符 变量名”,那么以整形数组指针为例就应该是:
(int [n]) (*p)。p表示一个指向包含n个整形元素的数组的指针变量。但据说因为这种形式看起来很不爽(其实我看着还是比较顺眼的..),所以C语言里把这个形式变换了一下,调整为int (*p)[n],这就是C语言里对数组指针的定义形式。
{ps:数组指针和指针数组:
(1)指针数组:首先是一个数组,其次,数组每个元素都是一个指针类型。如int *a[5];每个数组元素均是一个指向整型数据的指针。
(2)数组指针:首先是一个指针,该指针指向一个数组。如int(*a)[5];a表示一个指向“含有5个整型元素的数组”的指针。
}
说到这里,不必多费力可以猜出到二维数组和数组指针之间的某种联系。
那么,对于int a[2][3];int (*p)[3];
怎样设置p,才可以把p和a建立关联呢?
首先,p是一个地址,指向一个包含3个整形元素的数组。
a[2][3]数组的哪一项标识可以理解为“指向包含3个整形元素的数组”呢?
(1)是&a[0][0]么?
不是。&a[0][0]指向的是整型数据。
(2)是&a么?
不是。&a指向的是一个[2]*[3]的逻辑上的二维数组。
(3)是a[0]么?
不是。a[0]作为地址,指向的是a[0]这个数组首元素,即&a[0][0]。
(4)是&a[0]么?
是。前面说到a[0]可作为{1,2,3}这个数组的数组名,数组名的第一层含义是:表示{1,2,3}这个数组。那么对a[0]取地址运算,即&a[0]表示的是这个数组的首地址,对应单位为3*sizeof(int),和p对应的一致。
(5)是a么?
是。a表示地址时,表示的是等效数组的首元素的首地址,即a对应单位大小为sizeof(a[0]),即三个整形数据大小。与p所指一致。
上述(1)~(5)的分析可以通过如下示例观察是否有warning信息。
#include<stdio.h> int main() { int a[2][3]={1,2,3,4,5,6}; int (*p)[3]; p=a; //自行替换成其他形式,看看有无warning信息 printf("%p--%p",&a[0][0],p); getch(); return 0; }
现在我们知道如何把一个数组指针和一个二维数组绑定了;即:p=a;或者p=&a[0];这样p就可以设定指向了二维数组a的首地址。一般情况肯定是选用p=a;好记又方便。
这里重点说明一下:p,a都为地址,而且均为指向“包含3个整形数据的数组”,那么a作为地址,是个什么类型的地址?是“指向包含三个整形数据数组的地址”,所以a和p的指向类型一致,a可以当做“数组指针型”地址。这一点的理解是很有必要的,对今后的地址运算,函数的二维数组传参都很有用。
那么接着回到主题,“绑定”后如何通过p来访问、操作一个二维数组呢?依然先对几个形式做一下分析,先分析p,*p,**p。
以int a[2][3];int (*p)[3];p=a为例:
(1)p是一个指针,指向int[3]型数组;本例中p指向a[0]这个数组,那么(p+1)呢?自然是指向下一个数组a[1]首地址,即(p+1)=&a[1]。
(2)*p是什么?p是指向数组的指针,那么*p自然是一个数组咯,对于本例,*p就是a[0]这个数组,这里同时还明确:*p甚至可以看成这个数组的数组名使用,即具备数组名的两层含义:
第一层:*p表示a[0]这个数组。说到这里,感受是这真心不是一句废话,因为*p表示的是数组,那么&(*p)表示是对整个数组a[0]取首地址,则(&(*p)+1)表示的就应该是下一个数组首地址,即(&(*p)+1)=&a[1];而&(*p)不就是p嘛~看看上面的(1)中的结论:(p+1)=&a[1];=。=So这里也互相验证了一下)
第二层:*p表示a[0]这个数组的首元素的地址,即*p=&a[0][0];那么:
第一种情况:(*p+i)呢?*p的基准是数组里的元素,因此*p递增的单位是sizeof(int),(*p+i)就表示的是a[0]数组中下一个元素的首地址即&a[0][i];
第二种情况:*(p+i)呢?p的基准是数组,因此p递增的单位是sizeof(a[i]),(p+i)表示a[i]数组的首地址,*(p+i)则表示a[i]数组的首元素的地址
那么我要通过p访问二维数组中第i行第j列的元素应该如何表示?分三步:
(1)先定位到第i行首地址:(p+i)
(2)再定位到该行首元素地址:*(p+i)
(3)最终定位该行第j列元素地址:*(p+i)+j;对该地址进行取值*(*(p+i)+j);即有a[i][j]=*(*(p+i)+j);
对于二维数组名和对应的数组指针变量p,均可以通过下标和地址偏移的方式访问元素。
上述3.1和3.2都描述了对二维数组访问的方法,比较明显可以看出通过数组指针方式更为方便:
1、定位(绑定)容易:直接p=a就可以。
2、绑定后操作方便,可以通过下标和基址偏移两种方式进行。