预计阅读本页时间:-
指针和多维数组有什么关系?为什么我们需要知道它们之间的关系?函数是通过指针来处理多维数组的,因此在使用这样的函数之前,您需要更多地了解指针。对于第一个问题,让我们通过几个例子来找出答案。为简化讨论,我们采用比较小的数组。假设有如下的声明:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
数组名zippo同时也是数组首元素的地址。在本例中,zippo的首元素本身又是包含两个int的数组,因此zippo也是包含两个int的数组的地址。下面从指针属性进一步分析:
● 因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]相同。另一方面,zippo[0]本身是包含两个整数的数组,因此zippo[0]的值同其首元素(一个整数)的地址&zippo[0][0]相同。简单地说,zippo[0]是一个整数大小对象的地址,而zippo是两个整数大小对象的地址。因为整数和两个整数组成的数组开始于同一个地址,因此zippo和zippo[0]具有相同的数值。
● 对一个指针(也即地址)加1,会对原来的数值加上一个对应类型大小的数值。在这方面,zippo和zippo[0]是不一样的,zippo所指向对象的大小是两个int,而zippo[0]所指向对象的大小是一个int。因此zippo+1和zippo[0]+1的结果不同。
● 对一个指针(也即地址)取值(使用运算符*或者带有索引的[]运算符)得到的是该指针所指向对象的数值。因为zippo[0]是其首元素zippo[0][0]的地址,所以*(zippo[0])代表存储在zippo[0][0]中的数值,即一个int数值。同样,*zippo代表其首元素zippo[0]的值,但是zippo[0]本身就是一个int数的地址,即&zippo[0][0],因此*zippo是&zippo[0][0]。对这两个表达式同时应用取值运算符将得到**zippo等价于*&zippo[0][0],后者简化后即为一个int数zippo[0][0]。简言之,zippo是地址的地址,需要两次取值才可以得到通常的数值。地址的地址或指针的指针是双重间接(double indirection)的典型例子。
显然,增加数组维数会增加指针的复杂度。现在,大多数C初学者都会认识到为什么指针被认为是该语言中最难掌握的部分。认真地学习了前面所讲的内容后,请阅读程序清单10.15中的实例,此例显示了一些地址值和数组内容。
程序清单10.15 zippo1.c程序
在一个系统上,输出结果如下:
其他系统的输出结果会与上面的不同,但输出结果之间的关系是和上面的一样的。输出显示出二维数组zippo的地址和一维数组zippo[0]的地址是相同的,均为相应的数组首元素的地址,它的值是和&zippo[0][0]相同的。
然而,差别也是有的。在我们的系统上,int是4个字节长。前面我们讨论过,zippo[0]指向4字节长的数据对象。对zippo[0]加1导致它的值增加4。数组名zippo是包含两个int数的数组的地址,因此它指向8字节长的数据对象。所以,对zippo加1导致它的值增加8。
程序显示zippo[0]和*zippo是相同的,这点是正确的。另一方面,二维数组名必须两次取值才可以取出数组中存储的数据。这可以两次使用间接运算符(*)来实现,或两次使用方括号运算符([])(也可以采用一次*和一次[]来实现,但我们不讨论这么多的情况)。具体地:zippo[2][1]的等价指针符号表示为*(*(zippo+2)+1)。您应尽力去分析理解。表10.2中分步建立了这个表达式:
表10.2 分析*(*(zippo+2)+1)
zippo | 第1个大小为2个int的元素的地址 |
---|---|
zippo+2 | 第3个大小为2个int的元素的地址 |
*(zippo+2) | 第3个元素,即包含2个int值的数组,因此也是其第1个元素(int值)的地址 |
*(zippo+2)+1 | 包含2个int值的数组的第2个元素(int值)的地址 |
*(*(zippo+2)+1) | 数组第3行第2个int(zippo[2][1])的值 |
这里使用指针符号来显示数据的意图并不是为了说明可以用它替代更简单的zippo[2][1]表达式,而是要说明当您正好有一个指向二维数组的指针并需要取值时,最好不要使用指针符号,而应当使用形式更简单的数组符号。
图10.5以另一种视图显示了数组地址、数组内容和指针之间的关系。
图10.5 数组的数组
10.7.1 指向多维数组的指针
如何声明指向二维数组(如zippo)的指针变量pz?例如,在编写处理像zippo这样的数组的函数时,就会用到这类指针。指向int的指针可以胜任吗?不可以。这种指针只是和zippo[0]兼容,因为它们都指向一个单个int值。但是zippo是其首元素的地址,而该首元素又是包含两个int值的数组。因此,pz必须指向一个包含两个int值的数组,而不是指向一个单个int值。下面是正确的代码:
该语句表明pz是指向包含两个int值的数组的指针。为什么使用圆括号?因为表达式中[]的优先级高于*。因此,如果我们这样声明:
那么首先方括号和pax结合,表示pax是包含两个某种元素的数组。然后和*结合,表示pax是两个指针组成的数组。最后,用int来定义,表示pax是由两个指向int值的指针构成的数组。这种声明会创建两个指向单个int值的指针。但前面的版本通过圆括号使pz首先和*结合,从而创建一个指向包含两个int值的数组的指针。程序清单10.16显示了如何使用指向二维数组的指针。
程序清单10.16 zippo2.c程序
输出结果如下:
再次,不同的计算机得到的结果可能有些差别,但是相互关系是一样的。尽管pz是一个指针,而不是数组名,仍然可以使用pz[2][1]这样的符号。更一般地,要表示单个元素,可以使用数组符号或指针符号;并且在这两种表示中既可以使用数组名,也可以使用指针:
10.7.2 指针兼容性
指针之间的赋值规则比数值类型的赋值更严格。例如,您可以不需要进行类型转换就直接把一个int数值赋给一个double类型的变量。但对于指针来说,这样的赋值是不允许的:
这些规定也适用于更复杂的类型。假设有如下声明:
那么,有如下结论:
请注意,上面的非法赋值都包含着两个不指向同一类型的指针。例如,pt指向一个int数值,但是ar1指向由3个int值构成的数组。同样,pa指向由3个int值构成的数组,因此它与arl的类型一致,但是和ar2的类型不一致,因为ar2指向由2个int值构成的数组。
后面两个例子比较费解。变量p2是指向int的指针的指针,然而,ar2是指向由2个int值构成的数组的指针(简单一些说,是指向int[2]的指针)。因此p2和ar2类型不同,不能把ar2的值赋给p2。但是*p2的类型为指向int的指针,所以它和ar2[0]是兼容的。前面讲过,ar2[0]是指向其首元素ar2[0][0]的指针,因此ar2[0]也是指向int的指针。
一般地,多重间接运算不容易理解。例如,考虑下面这段代码:
正如前面所提到的,把const指针赋给非const指针是错误的,因为您可能会使用新指针来改变const数据。但是把非const指针赋给const指针是允许的,这样的赋值有一个前提:只进行一层间接运算:
在进行两层间接运算时,这样的赋值不再安全。如果允许这样赋值,可能会产生如下的问题:
10.7.3 函数和多维数组
如果要编写处理二维数组的函数,首先需要很好地理解指针以便正确声明函数的参数。在函数体内,通常可以使用数组符号来避免使用指针。
下面我们编写一个处理二维数组的函数。一种方法是把处理一维数组的函数应用到二维数组的每一行上,也就是如下所示这样处理:
如果junk是二维数组,那么junk[i]就是一维数组,可以把它看做是二维数组的一行。函数sum()计算二维数组每行的和,然后由for循环把这些和加起来得到“总和”。
然而,使用这种方法得不到行列信息。在这个应用程序(求总和)中,行列的信息不重要,但是假设每行代表一年,每列代表一月,则可能需要一个函数来计算某个列的和。这种情况下,函数需要知道行列的信息。要具有行列信息,需要恰当地声明形参变量以便于函数能够正确地传递数组。在本例中,数组junk是3行4列的int数组。如前面讨论中所指出的,这表明junk是指向由4个int值构成的数组的指针。声明此类函数参量的方法如下所示:
当且仅当pt是函数的形式参量时,也可以作如下这样声明:
注意到第一对方括号里是空的。这个空的方括号表示pt是一个指针,这种变量的使用方法和junk一样。程序清单10.17中的例子就将使用上面两种声明的方法。注意清单中展示了原型语法的3种等价形式。
程序清单10.17 array2d.c程序
输出结果如下:
程序清单10.17中的程序把数组名junk(即指向首元素的指针,首元素是子数组)和符号常量ROWS(代表行数,数值为3)做为参数传递给函数。每个函数都把ar看做是指向包含4个int值的数组的指针。列数是在函数体内定义的,但是行数是靠函数传递得到的。这个函数可以工作在多种情况下。例如,如果把12做为行数传递给函数,则它可以处理12行4列的数组。这是因为rows是元素的数目;然而,每个元素都是一个数组,或者看做一行,rows也就可以看做是行数。
请注意ar的使用方式同main()中junk的使用方式一样。这是因为ar和junk是同一类型,它们都是指向包含4个int值的数组的指针。
请注意下面的声明是不正确的:
回忆一下,编译器会把数组符号转换成指针符号。这就意味着,(例如)ar[1]会被转换成ar+1。编译器这样转换的时候需要知道ar所指向对象的数据大小。下面的声明:
就表示ar指向由4个int值构成的数组,也就是16个字节长(本系统上)的对象,所以ar+1表示“在这个地址上加16个字节大小”。如果是空括号,则编译器将不能正确处理。
也可以如下这样在另一对方括号中填写大小,但编译器将忽略之:
与使用typedef相比,这种形式要方便得多:
一般地,声明N维数组的指针时,除了最左边的方括号可以留空之外,其他都需要填写数值。
这是因为首方括号表示这是一个指针,而其他方括号描述的是所指向对象的数据类型。请参看下面的等效原型表示:
此处ar指向一个12x20x30的int数组。