关于C之二维数组与指针(首地址&步长)

先看一个二维数组的直观示意图:

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); // 标准形式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值