先看一个二维数组的直观示意图:
float rain[5][12]:5行12列。二维数组可以看作是把一维数组当作元素的一维数组。rain[5][12]就可看作是由5个元素的组成的一维数组(每个元素都是一个一维数组,其size为12)。
可以这样初始化:
const float rain[5][12] = {
{4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
{8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
{9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
{7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
{7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}};
第1个元素rain[0] = {4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6}
第5个元素rain[4] = {7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}
初始化一维数组时的格式如下:
sometype ar1[5] = {val1, val2, val3, val4, val5};
对于rain[5][12],只不过val1 = {4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6}
我们知道,对于一维数组,数组变量名即是指向首个元素的指针变量。对于二维数组也一样,数组变量名也是指向首个元素的指针变量。
如下,如果flizny是一个二维数组,那么下面恒成立:
flizny == &flizny[0];
我们知道,一维数组的元素都是在内存空间是连续分布的,对于二维数组也是如此。
讨论二维数组前,先对一维数组与指针作必要的了解:
先看一个打印一维数组元素地址的例子,更直观了解地址的分配:
#include <stdio.h>
#define SIZE 4
int main(void) {
short dates[SIZE];
short* pti;
short index;
double bills[SIZE];
double* ptf;
pti = dates; // assign address of array to pointer
ptf = bills;
printf("%23s %15s\n", "short", "double");
for (index = 0; index < SIZE; index++)
printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);
return 0;
}
short double
pointers + 0: 0x7fff5fbff8dc 0x7fff5fbff8a0
pointers + 1: 0x7fff5fbff8de 0x7fff5fbff8a8
pointers + 2: 0x7fff5fbff8e0 0x7fff5fbff8b0
pointers + 3: 0x7fff5fbff8e2 0x7fff5fbff8b8
通过程序运行结果及直观图,可以看到,一维数组元素的内容空间地址是连续分配的。
我们知道,数组和指针取值是可以等效的:
dates + 2 == &date[2] // same address
*(dates + 2) == dates[2] // same value
=> *(dates + index) == dates[index]
注意:dates != dates[0],因为二维数组名是代表首元素(是一行)的地址,而不是首行第一个元素的地址!
三维数组或者更多维的数组都是一样的,我们只需要把数组名当做变量最近的那个方括号[]里面内容的地址就行了。比如char ar[30]="add";ar代表的首元素地址;char b2r[3][30]={"abc","bcd","ddd"};b2r代表的是最近方括号是谁?是行,那么b2r代表的就是行的地址;三维的话,不知道叫啥,姑且就叫栋吧,就是栋的地址。然后在进行推导。所以上面dates这个指向第一行的指针是不等于dates[0]这个第一行元素指针的。
看下面一个简单例子,进一步解释:
#include<stdio.h>
int main(void) {
char zippo[2][3] = {
{'a', 'b', 'c'},
{'X', 'Y', 'Z'}
};
printf("%p,%p,%p\n", zippo, zippo[0], &zippo[0][0]);
int zippoAddr = *zippo;
printf("%d,%c,%c,%c\n", zippoAddr, *zippo, *zippo[0], **zippo);
printf("%zd,%zd\n", sizeof(zippo), sizeof(zippo[0]));
printf("%d,%d\n", *(zippo + 1) - *zippo, *(zippo[0] + 1) - *zippo[0]);
}
0030FE50,0030FE50,0030FE50
3210832,P,a,a
6,3
3,1
可以看到,总有恒等式zippo==zippo[0]==&zippo[0][0]。sizeof(zippo)=r×c=2×3=6,sizeof(zippo[0])=c=3。步长是指指针变量+1后的偏移字节量;&zippo是整个二维数组的地址,&zippo[m]是某行一维数组的地址,&zippo[m][n]是某个元素的地址;对于指针zippo,步长=sizeof(zippo[0])=c×sizeof(char)=3;指针zippo[0],步长=sizeof(zippo[0][0])=sizeof(char)=1。
既然zippo==zippo[0],为什么*zippo=不确定字符, *zippo[0]=='a'?
*zippo[0]=第一行首元素地址取值=zippo[0][0]='a',这个没有疑问。
如上代码,当某次运行得到zippoAddr(*zippo)=3210832时,printf("%c", *zippo)=='P'。显然3210832对于%c是溢出了,对于可显常用字符是在[0,127]区间的。3210832(十进制)==001100001111111001010000(二进制),取低8位=01010000对应的就是ASCII码的'P'字符。所以说,用%c打印指针变量就是有问题的,指针变量32位系统上通常占4字节,而char占1字节,所以会溢出;%c只能用来打印存储char的内容。
arr2与arr1与arr对应的首地址的值是一样的,如图每个方框左上尖角其实是重合的。
由于所有数组的元素都是连续的,所以对于二维数组,如下图更直观地展示连续:
如果int arr2d[2][3]={{1,2,3},{7,8,9}};则arr2d[0]+3 == &arr2d[1][0]。如下程序实测结果也说明了这一点。
#include<stdio.h>
int main(void) {
int arr2d[2][3] = { {1,2,3},{7,8,9} };
printf("%p\n%p\n", arr2d[0] + 3, &arr2d[1][0]);
}
00DBF79C
00DBF79C
对于一维数组a,你知道a、&a、&a[0]的区别吗?
#include <stdio.h>
int main() {
int a[] = {0,1,2,3,4,5,6,7,8,9};
printf("&a[0]的地址是 : %p\n", &a[0]);
printf("a的地址是 : %p\n", a);
printf("&a的地址是 : %p\n\n", &a);
printf(">>>>>>测试步长<<<<<<\n");
printf("&a[0]的地址是 : %p\n", &a[0] + 1);
printf("a的地址是 : %p\n", a + 1);
printf("&a的地址是 : %p\n", &a + 1);
return 0;
}
在运行之前我们先分析一下:
首先,a即是指向数组首个元素的地址的指针,所以对于地址a==&a[0],没毛病。
那么,&a则是对a进行了一次取地址操作。而对a取地址性质不是对&a[0]再次取地址,性质变成了对整个数组取地址。但都开始于同一地址。毕竟地址就是一个int型值而已。这样推断的话,对于地址a==&a。这样一来,对于地址a==&a[0]==&a了。但由于&a是整个数组的地址,a是首元素的地址,所以它们的步长一定不相等。&a的步长即&a+1中“1”的含义,“1”==sizeof(a)==10*sizeof(int),而对于a的步长“1”==sizeof(int)==4。
&a[0]的地址是 : 00F5FC78
a的地址是 : 00F5FC78
&a的地址是 : 00F5FC78
>>>>>>测试步长<<<<<<
&a[0]的地址是 : 00F5FC7C
a的地址是 : 00F5FC7C
&a的地址是 : 00F5FCA0
通过运行结果可知:对于地址a==&a[0]==&a。并且00F5FC78+sizeof(int)==00F5FC7C,00F5FC78+sizeof(10*int))==00F5FCA0证实了a或&a[0]与&a的步长是不同的:a+1 == &a[0]+1 != &a+1。
其实步长的不同就能确定类型的不同,如下图报错,就可知:
因为par是指向整个一维数组的指针而不是指向首元素单个元素的指针,类型只能与&ar匹配。
对于一维数组,数组变量名即是一个指针,而对于二维数组,数组变量名就是一个指针的指针。推而广之,三维数组变量名就是一个指针的指针的指针。对于zippo[2][3]而言,zippo就是指针zippo[0]的指针,所以:
**zippo == *zippo[0] == 'a'
=> &(**zippo) == &(*zippo[0]) == &('a')
=> *zippo == zippo[0] == &('a')
=> zippo == &zippo[0] == &&('a')
关于指针初始化要注意:
int * pt; // 一个未初始化的指针
*pt = 5; // a terrible error
double * pd; // 一个未初始化的指针
*pd = 2.4; // DON'T DO IT
int * pt;即声明pt为一个指向int类型的指针变量,即pt存的是一个地址,而*pt=5;就是将指针所指向的内存空间赋值为5,然而pt是一个未初始化的指针(野指针),不知指向哪个地址,所以5这个值不知存于何处,所以是一个“terrible error”。对于double * pd;是一样的道理。如果通过mallloc()给分配了内存空间,地址赋给pt,这样才是没问题的。
现给定:
int urn[3];
int * ptr1, * ptr2;
下面是有效和无效的操作:
有效 | 无效 |
ptr1++; | urn++; |
ptr2 = ptr1 + 2; | ptr2 = ptr2 + ptr1; |
ptr2 = urn + 1; | ptr2 = urn * ptr1; |
关于urn++无效,其实在关于字符串两种声明方式的核心区别有提到。只不过那里是字符数组,道理一样。
因为"hello"是常量,所以所有指向它的指针都不能修改它的值。故指针pc、pc2都不能修改它,否则段错误。
因为farr是数组,而所有数组名都是【指针常量】,所以farr的指向不能变,否则段错误。假设farr++;正确,则farr[0]是啥?是3.4?显然就不能通过数组名下标访问元素了。
因为parr不是数组名,所以它不是指针常量,从而可以改变指向。
char *ptstr="some string";说明指针ptstr变量指向的是"some string"这个常量字符串的首地址,而这个地址下对应的内容是"some string",这里ptstr是一个常量指针。anytype arr[]中arr是一个指针常量,这也是一般数组的默认属性,这也是为什么不能通过数组名互相赋值的原因了。下面是恒等的:
char *pt = "some string"; <=> const char *pt = "some string";
anytype arr[];其中 arr <=> anytype * const arr
所以,urn这个指针常量所指向的地址是不能变的,urn++改变了地址,显然出错。至于随便的地址相加加乘这种操作会导致未知无法预测的地址,所以有问题。
接下来正式进入主题——二维数组:
int zippo[4][2];
由于二维数组其中第个元素都是一个一维数组,所以zippo[m]表示的是二维数组第m个元素的指针变量。
zippo就是一个指针的指针(是指针zippo[0]的指针)。zippo表示首个元素(一行)的地址,即:
zippo==&zippo[0] => *zippo==zippo[0] => **zippo==zippo[0][0]
zippo+2==&zippo[2] => *(zippo+2)==zippo[2] => **(zippo+2)==zippo[2][0]
*(zippo+2)==zippo[2] => *(zippo+2)+1==zippo[2]+1 => *(*(zippo+2)+1)==zippo[2][1]
推而广之:
zippo[m][n] == *(*(zippo + m) + n)
如何声明一个指针变量pz指向一个二维数组,比如指向zippo?
由于zippo[4][2]的每个元素都是一个包含2个int类型值的一维数组,因此pz必须指向一个内含2个int类型值的数组,其声明如下:
int (* pz)[2]; ←(优先级* < []),所以用括号表明pz是一个指针,后面[2]和前面int表明pz是一个指向内含两个int类型值的数组的变量
pz = zippo;
以上代码把pz声明为指向一个数组的指针,该数组内含两个int类型值。为什么要在声明中使用圆括号?因为[]的优先级高于*。
如果去掉圆括号:
int * pax[2]; ← pax是一个内含两个指针元素的数组,每个元素都指向int的指针
由于[]优先级高,先与pax结合,所以pax成为一个内含两个元素的数组。然后*表示pax数组内含两个指针。这行代码声明了两个指向int的指针。即它是一个指针数组,每个元素都是int型指针,数组大小为2。
C99新增了变长数组(variable-length array,VLA),允许使用 变量表示数组的维度。如下所示:
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; // 一个变长数组(VLA)
变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是在创建数组时,可以使用变量指定数组的维度。
如下定义一个有变长数组参数的函数:
int sum2d(int rows, int cols, int ar[rows][cols]) {
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < cols; c++)
tot += ar[r][c];
return tot;
}
C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:
上面函数原型可写为:int sum2d(int, int, int ar[*][*]);
对于形参是二维数组的函数,这里有必要说明一下:
编译器会把数组表示法转换成指针表示法。例如,编译器会把 ar[1]转换成 ar+1。编译器对ar+1求值,要知道ar所指向的对象大小。下面的声明:
int sum2(int ar[][4], int rows); // 有效声明
表示ar指向一个内含4个int类型值的数组(在我们的系统中,ar指向的对象占16字节),所以ar+1的意思是“该地址加上16字节”。如果第2对方括号是空的,编译器就不知道该怎样处理。
int sum2(int ar[3][4], int rows); // 有效声明,但是3将被忽略
访问指针表示法的二维数组:
#include <stdio.h>
int main() {
int ar[6] = { 1, 7, 15, 23, 31, 59 };
int(*par)[6] = &ar;
printf(" (*par)[4] = %d\n", (*par)[4]);
printf("(*(*par + 4)) = %d\n", (*(*par + 4)));
printf("*(par[0] + 4) = %d\n", *(par[0] + 4));
printf(" par[0][4] = %d\n", par[0][4]);
puts("*************************");
int zippo[4][2] = {
{1,2},
{5,6},
{9,10},
{13,14}
};
int(*pz)[2] = zippo;
printf("*(*(pz + 2) + 1) = %d\n", *(*(pz + 2) + 1));
printf(" *(pz[2] + 1) = %d\n", *(pz[2] + 1));
printf(" pz[2][1] = %d", pz[2][1]);
return 0;
}
(*par)[4] = 31
(*(*par + 4)) = 31
*(par[0] + 4) = 31
par[0][4] = 31
*************************
*(*(pz + 2) + 1) = 10
*(pz[2] + 1) = 10
pz[2][1] = 10
关键晦涩点在于分隔线上半部分:其实只要把ar[6]当作是ar[1][6],就如同分隔线下面一样了。我们知道数组表示法本质上就是指针,编译器也会把数组表示法转换成指针表示法,所以指向数组的指针变量可以直接用[]下标去访问数组元素。int(*par)[6] = &ar;即par被声明为一个指向包含6个int型元素数组的指针,注意是&ar而不是ar,ar表示指向首元素的指针,&ar才表示整个数组的指针,它们的步长是不一样的。par前面加解引用运算符*后(*par)就等同于*&ar,即(*par)<=>ar,(*par)[N]==ar[N],之所以加()是因为*优先级低于[]。
分隔线下半部分:pz+2代表第3行的行首地址,*(pz+2)代表第3行首元素的地址,即*(pz+2)==pz[2]。其他就不用多说了。
最后,我们讨论用typedef定义形参类型:
typedef int arr4[4]; // arr4是一个内含4个int的数组
typedef arr4 arr3x4[3]; // arr3x4是一个内含3个arr4的数组
int sum2(arr3x4 ar, int rows); // 与下面的声明相同
int sum2(int ar[3][4], int rows); // 与下面的声明相同
int sum2(int ar[][4], int rows); // 标准形式